From 892a50a3b77165b0c7e630545db5050f2eee55a2 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Tue, 27 Aug 2024 15:10:26 +0200 Subject: [PATCH] Performance: Load shared frontend plugin dependencies on demand (#87644) * feat(plugins): only load shared plugin dependencies when needed * feat(plugins): add react-redux and fix up comments * feat(plugins): attempt to load async deps in fe sandbox * feat(frontend): defer script execution to prevent systemjs from loading app.js --- .../plugins/loader/sharedDependencies.ts | 73 ++++++------------- .../app/features/plugins/loader/systemjs.ts | 2 + public/app/features/plugins/loader/utils.ts | 41 +++++++++-- .../plugins/sandbox/plugin_dependencies.ts | 2 +- .../plugins/sandbox/sandbox_plugin_loader.ts | 8 +- public/views/index.html | 1 + 6 files changed, 66 insertions(+), 61 deletions(-) diff --git a/public/app/features/plugins/loader/sharedDependencies.ts b/public/app/features/plugins/loader/sharedDependencies.ts index 3f9c3d10349..dbb8ed8692f 100644 --- a/public/app/features/plugins/loader/sharedDependencies.ts +++ b/public/app/features/plugins/loader/sharedDependencies.ts @@ -1,25 +1,4 @@ -import * as emotion from '@emotion/css'; -import * as emotionReact from '@emotion/react'; -import * as kusto from '@kusto/monaco-kusto'; -import * as d3 from 'd3'; -import * as i18next from 'i18next'; import jquery from 'jquery'; -import _ from 'lodash'; // eslint-disable-line lodash/import-scope -import moment from 'moment'; // eslint-disable-line no-restricted-imports -import prismjs from 'prismjs'; -import react from 'react'; -import reactDom from 'react-dom'; -import * as reactInlineSvg from 'react-inlinesvg'; -import * as reactRedux from 'react-redux'; // eslint-disable-line no-restricted-imports -import * as reactRouterDom from 'react-router-dom'; -import * as reactRouterCompat from 'react-router-dom-v5-compat'; -import * as redux from 'redux'; -import * as rxjs from 'rxjs'; -import * as rxjsOperators from 'rxjs/operators'; -import slate from 'slate'; -import slatePlain from 'slate-plain-serializer'; -import slateReact from 'slate-react'; - import 'vendor/flot/jquery.flot'; import 'vendor/flot/jquery.flot.selection'; import 'vendor/flot/jquery.flot.time'; @@ -65,14 +44,14 @@ const jQueryFlotDeps = [ 'jquery.flot', ].reduce((acc, flotDep) => ({ ...acc, [flotDep]: { fakeDep: 1 } }), {}); -export const sharedDependenciesMap: Record = { - '@emotion/css': emotion, - '@emotion/react': emotionReact, +export const sharedDependenciesMap = { + '@emotion/css': () => import('@emotion/css'), + '@emotion/react': () => import('@emotion/react'), '@grafana/data': grafanaData, '@grafana/runtime': grafanaRuntime, - '@grafana/slate-react': slateReact, // for backwards compatibility with older plugins + '@grafana/slate-react': () => import('slate-react'), '@grafana/ui': grafanaUI, - '@kusto/monaco-kusto': kusto, + '@kusto/monaco-kusto': () => import('@kusto/monaco-kusto'), 'app/core/app_events': { default: appEvents, __useDefault: true, @@ -102,29 +81,23 @@ export const sharedDependenciesMap: Record = { 'app/features/dashboard/impression_store': { impressions: impressionSrv, }, - d3: d3, - emotion: emotion, + d3: () => import('d3'), + emotion: () => import('@emotion/css'), // bundling grafana-ui in plugins requires sharing i18next state - i18next: i18next, + i18next: () => import('i18next'), jquery: { default: jquery, __useDefault: true, }, ...jQueryFlotDeps, - lodash: { - default: _, - __useDefault: true, - }, - moment: { - default: moment, - __useDefault: true, - }, - prismjs: prismjs, - react: react, - 'react-dom': reactDom, + lodash: () => import('lodash').then((module) => ({ ...module, __useDefault: true })), + moment: () => import('moment').then((module) => ({ ...module, __useDefault: true })), + prismjs: () => import('prismjs'), + react: () => import('react'), + 'react-dom': () => import('react-dom'), // bundling grafana-ui in plugins requires sharing react-inlinesvg for the icon cache - 'react-inlinesvg': reactInlineSvg, - 'react-redux': reactRedux, + 'react-inlinesvg': () => import('react-inlinesvg'), + 'react-redux': () => import('react-redux'), // Migration - React Router v5 -> v6 // ================================= // Plugins that still use "react-router-dom@v5" don't depend on react-router directly, so they will not use this import. @@ -137,12 +110,12 @@ export const sharedDependenciesMap: Record = { // just exposing "react-router-dom-v5-compat". // // (This means that we are exposing two versions of the same package). - 'react-router-dom': reactRouterDom, // react-router-dom@v5 - 'react-router': reactRouterCompat, // react-router-dom@v6, react-router@v6 (included) - redux: redux, - rxjs: rxjs, - 'rxjs/operators': rxjsOperators, - slate: slate, - 'slate-plain-serializer': slatePlain, - 'slate-react': slateReact, + 'react-router-dom': () => import('react-router-dom'), + 'react-router': () => import('react-router-dom-v5-compat'), + redux: () => import('redux'), + rxjs: () => import('rxjs'), + 'rxjs/operators': () => import('rxjs/operators'), + slate: () => import('slate'), + 'slate-plain-serializer': () => import('slate-plain-serializer'), + 'slate-react': () => import('slate-react'), }; diff --git a/public/app/features/plugins/loader/systemjs.ts b/public/app/features/plugins/loader/systemjs.ts index 8b9c40fe749..5fb00018f91 100644 --- a/public/app/features/plugins/loader/systemjs.ts +++ b/public/app/features/plugins/loader/systemjs.ts @@ -1,6 +1,8 @@ import 'systemjs/dist/system'; // Add ability to load plugins bundled as AMD format import 'systemjs/dist/extras/amd'; +// Add named register for on demand dependency loading +import 'systemjs/dist/extras/named-register.js'; // Add ability to load plugins bundled as CJS format import 'systemjs-cjs-extra'; diff --git a/public/app/features/plugins/loader/utils.ts b/public/app/features/plugins/loader/utils.ts index ddff73cab09..2407852f6ee 100644 --- a/public/app/features/plugins/loader/utils.ts +++ b/public/app/features/plugins/loader/utils.ts @@ -3,7 +3,6 @@ import { config } from '@grafana/runtime'; import { sandboxPluginDependencies } from '../sandbox/plugin_dependencies'; import { SHARED_DEPENDENCY_PREFIX } from './constants'; -import { trackPackageUsage } from './packageMetrics'; import { SystemJS } from './systemjs'; export function buildImportMap(importMap: Record) { @@ -11,14 +10,9 @@ export function buildImportMap(importMap: Record) { // Use the 'package:' prefix to act as a URL instead of a bare specifier const module_name = `${SHARED_DEPENDENCY_PREFIX}:${key}`; - // get the module to use - const module = config.featureToggles.pluginsAPIMetrics ? trackPackageUsage(importMap[key], key) : importMap[key]; + // expose dependency to loaders + addPreload(module_name, importMap[key]); - // expose dependency to SystemJS - SystemJS.set(module_name, module); - - // expose dependency to sandboxed plugins - // the sandbox handles its own way of plugins api metrics sandboxPluginDependencies.set(key, importMap[key]); acc[key] = module_name; @@ -26,6 +20,37 @@ export function buildImportMap(importMap: Record) { }, {}); } +function addPreload(id: string, preload: (() => Promise) | System.Module) { + if (SystemJS.has(id)) { + return; + } + + let resolvedId; + try { + resolvedId = SystemJS.resolve(id); + } catch (e) { + console.log(e); + } + + if (resolvedId && SystemJS.has(resolvedId)) { + return; + } + + const moduleId = resolvedId || id; + if (typeof preload === 'function') { + SystemJS.register(id, [], (_export) => { + return { + execute: async function () { + const module = await preload(); + _export(module); + }, + }; + }); + } else { + SystemJS.set(moduleId, preload); + } +} + export function isHostedOnCDN(path: string) { return Boolean(config.pluginsCDNBaseURL) && path.startsWith(config.pluginsCDNBaseURL); } diff --git a/public/app/features/plugins/sandbox/plugin_dependencies.ts b/public/app/features/plugins/sandbox/plugin_dependencies.ts index 90503ae2b41..cfe40b9002e 100644 --- a/public/app/features/plugins/sandbox/plugin_dependencies.ts +++ b/public/app/features/plugins/sandbox/plugin_dependencies.ts @@ -2,4 +2,4 @@ * Map with all dependencies that are exposed to plugins sandbox * e.g.: @grafana/ui, @grafana/data, etc... */ -export const sandboxPluginDependencies = new Map([]); +export const sandboxPluginDependencies = new Map Promise)>([]); diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts b/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts index c3c278cb5bf..791e791ec3a 100644 --- a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts +++ b/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts @@ -162,7 +162,7 @@ async function doImportPluginModuleInSandbox(meta: SandboxPluginMeta): Promise [[end]]