mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
1e32e98bf6
commit
892a50a3b7
@ -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<string, System.Module> = {
|
||||
'@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<string, System.Module> = {
|
||||
'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<string, System.Module> = {
|
||||
// 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'),
|
||||
};
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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<string, System.Module>) {
|
||||
@ -11,14 +10,9 @@ export function buildImportMap(importMap: Record<string, System.Module>) {
|
||||
// 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<string, System.Module>) {
|
||||
}, {});
|
||||
}
|
||||
|
||||
function addPreload(id: string, preload: (() => Promise<System.Module>) | 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);
|
||||
}
|
||||
|
@ -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<string, System.Module>([]);
|
||||
export const sandboxPluginDependencies = new Map<string, System.Module | (() => Promise<System.Module>)>([]);
|
||||
|
@ -162,7 +162,7 @@ async function doImportPluginModuleInSandbox(meta: SandboxPluginMeta): Promise<S
|
||||
}
|
||||
|
||||
try {
|
||||
const resolvedDeps = resolvePluginDependencies(dependencies, meta);
|
||||
const resolvedDeps = await resolvePluginDependencies(dependencies, meta);
|
||||
// execute the plugin's code
|
||||
const pluginExportsRaw = factory.apply(null, resolvedDeps);
|
||||
// only after the plugin has been executed
|
||||
@ -220,7 +220,7 @@ async function doImportPluginModuleInSandbox(meta: SandboxPluginMeta): Promise<S
|
||||
* https://github.com/requirejs/requirejs/wiki/Differences-between-the-simplified-CommonJS-wrapper-and-standard-AMD-define#magic
|
||||
*
|
||||
*/
|
||||
function resolvePluginDependencies(deps: string[], pluginMeta: SandboxPluginMeta) {
|
||||
async function resolvePluginDependencies(deps: string[], pluginMeta: SandboxPluginMeta) {
|
||||
const pluginExports = {};
|
||||
const pluginModuleDep: ModuleMeta = {
|
||||
id: pluginMeta.id,
|
||||
@ -232,6 +232,10 @@ function resolvePluginDependencies(deps: string[], pluginMeta: SandboxPluginMeta
|
||||
const resolvedDeps: CompartmentDependencyModule[] = [];
|
||||
for (const dep of deps) {
|
||||
let resolvedDep = sandboxPluginDependencies.get(dep);
|
||||
|
||||
if (typeof resolvedDep === 'function') {
|
||||
resolvedDep = await resolvedDep();
|
||||
}
|
||||
if (resolvedDep?.__useDefault) {
|
||||
resolvedDep = resolvedDep.default;
|
||||
}
|
||||
|
@ -331,6 +331,7 @@
|
||||
nonce="[[$.Nonce]]"
|
||||
src="[[$asset.FilePath]]"
|
||||
type="text/javascript"
|
||||
defer
|
||||
></script>
|
||||
[[end]]
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user