️ add speedy mode for sandbox to improve performance (#2271)

This commit is contained in:
Kuitos 2022-09-18 22:28:46 +08:00 committed by GitHub
parent a04e9ebf4d
commit 7f517264bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 85 additions and 70 deletions

View File

@ -102,7 +102,7 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.10.5", "@babel/runtime": "^7.10.5",
"import-html-entry": "^1.9.0", "import-html-entry": "^1.14.0",
"lodash": "^4.17.11", "lodash": "^4.17.11",
"single-spa": "^5.9.2" "single-spa": "^5.9.2"
}, },

View File

@ -75,6 +75,7 @@ type QiankunSpecialOpts = {
* @deprecated We use strict mode by default * @deprecated We use strict mode by default
*/ */
loose?: boolean; loose?: boolean;
speedy?: boolean;
patchers?: Patcher[]; patchers?: Patcher[];
}; };
/* /*

View File

@ -18,6 +18,7 @@ import type {
ObjectType, ObjectType,
} from './interfaces'; } from './interfaces';
import { createSandboxContainer, css } from './sandbox'; import { createSandboxContainer, css } from './sandbox';
import { lexicalGlobals } from './sandbox/common';
import { import {
Deferred, Deferred,
genAppInstanceIdByName, genAppInstanceIdByName,
@ -310,6 +311,7 @@ export async function loadApp<T extends ObjectType>(
let mountSandbox = () => Promise.resolve(); let mountSandbox = () => Promise.resolve();
let unmountSandbox = () => Promise.resolve(); let unmountSandbox = () => Promise.resolve();
const useLooseSandbox = typeof sandbox === 'object' && !!sandbox.loose; const useLooseSandbox = typeof sandbox === 'object' && !!sandbox.loose;
const speedySandbox = typeof sandbox === 'object' && !!sandbox.speedy;
let sandboxContainer; let sandboxContainer;
if (sandbox) { if (sandbox) {
sandboxContainer = createSandboxContainer( sandboxContainer = createSandboxContainer(
@ -320,6 +322,7 @@ export async function loadApp<T extends ObjectType>(
useLooseSandbox, useLooseSandbox,
excludeAssetFilter, excludeAssetFilter,
global, global,
speedySandbox,
); );
// 用沙箱的代理对象作为接下来使用的全局对象 // 用沙箱的代理对象作为接下来使用的全局对象
global = sandboxContainer.instance.proxy as typeof window; global = sandboxContainer.instance.proxy as typeof window;
@ -338,7 +341,9 @@ export async function loadApp<T extends ObjectType>(
await execHooksChain(toArray(beforeLoad), app, global); await execHooksChain(toArray(beforeLoad), app, global);
// get the lifecycle hooks from module exports // get the lifecycle hooks from module exports
const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox); const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox, {
scopedGlobalVariables: speedySandbox ? lexicalGlobals : [],
});
const { bootstrap, mount, unmount, update } = getLifecyclesFromExports( const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(
scriptExports, scriptExports,
appName, appName,

View File

@ -7,6 +7,7 @@ import { isBoundedFunction, isCallable, isConstructable } from '../utils';
type AppInstance = { name: string; window: WindowProxy }; type AppInstance = { name: string; window: WindowProxy };
let currentRunningApp: AppInstance | null = null; let currentRunningApp: AppInstance | null = null;
/** /**
* get the app that running tasks at current tick * get the app that running tasks at current tick
*/ */
@ -20,6 +21,7 @@ export function setCurrentRunningApp(appInstance: { name: string; window: Window
} }
const functionBoundedValueMap = new WeakMap<CallableFunction, CallableFunction>(); const functionBoundedValueMap = new WeakMap<CallableFunction, CallableFunction>();
export function getTargetValue(target: any, value: any): any { export function getTargetValue(target: any, value: any): any {
/* /*
isCallable && !isBoundedFunction && !isConstructable window.consolewindow.atob Illegal invocation isCallable && !isBoundedFunction && !isConstructable window.consolewindow.atob Illegal invocation
@ -77,15 +79,29 @@ export function getTargetValue(target: any, value: any): any {
return value; return value;
} }
const getterInvocationResultMap = new WeakMap<CallableFunction, any>(); export const unscopedGlobals = [
'undefined',
'Array',
'Object',
'String',
'Boolean',
'Math',
'Number',
'Symbol',
'parseFloat',
'Float32Array',
'isNaN',
'Infinity',
'Reflect',
'Float64Array',
'Function',
'Map',
'NaN',
'Promise',
'Proxy',
'Set',
'parseInt',
'requestAnimationFrame',
];
export function getProxyPropertyValue(getter: CallableFunction) { export const lexicalGlobals = [...unscopedGlobals, 'globalThis', 'window', 'self'];
const getterResult = getterInvocationResultMap.get(getter);
if (!getterResult) {
const result = getter();
getterInvocationResultMap.set(getter, result);
return result;
}
return getterResult;
}

View File

@ -29,6 +29,7 @@ export { css } from './patchers';
* @param useLooseSandbox * @param useLooseSandbox
* @param excludeAssetFilter * @param excludeAssetFilter
* @param globalContext * @param globalContext
* @param speedySandBox
*/ */
export function createSandboxContainer( export function createSandboxContainer(
appName: string, appName: string,
@ -37,6 +38,7 @@ export function createSandboxContainer(
useLooseSandbox?: boolean, useLooseSandbox?: boolean,
excludeAssetFilter?: (url: string) => boolean, excludeAssetFilter?: (url: string) => boolean,
globalContext?: typeof window, globalContext?: typeof window,
speedySandBox?: boolean,
) { ) {
let sandbox: SandBox; let sandbox: SandBox;
if (window.Proxy) { if (window.Proxy) {
@ -46,7 +48,14 @@ export function createSandboxContainer(
} }
// some side effect could be be invoked while bootstrapping, such as dynamic stylesheet injection with style-loader, especially during the development phase // some side effect could be be invoked while bootstrapping, such as dynamic stylesheet injection with style-loader, especially during the development phase
const bootstrappingFreers = patchAtBootstrapping(appName, elementGetter, sandbox, scopedCSS, excludeAssetFilter); const bootstrappingFreers = patchAtBootstrapping(
appName,
elementGetter,
sandbox,
scopedCSS,
excludeAssetFilter,
speedySandBox,
);
// mounting freers are one-off and should be re-init at every mounting time // mounting freers are one-off and should be re-init at every mounting time
let mountingFreers: Freer[] = []; let mountingFreers: Freer[] = [];
@ -76,7 +85,7 @@ export function createSandboxContainer(
/* ------------------------------------------ 2. 开启全局变量补丁 ------------------------------------------*/ /* ------------------------------------------ 2. 开启全局变量补丁 ------------------------------------------*/
// render 沙箱启动时开始劫持各类全局监听,尽量不要在应用初始化阶段有 事件监听/定时器 等副作用 // render 沙箱启动时开始劫持各类全局监听,尽量不要在应用初始化阶段有 事件监听/定时器 等副作用
mountingFreers = patchAtMounting(appName, elementGetter, sandbox, scopedCSS, excludeAssetFilter); mountingFreers = patchAtMounting(appName, elementGetter, sandbox, scopedCSS, excludeAssetFilter, speedySandBox);
/* ------------------------------------------ 3. 重置一些初始化时的副作用 ------------------------------------------*/ /* ------------------------------------------ 3. 重置一些初始化时的副作用 ------------------------------------------*/
// 存在 rebuilder 则表明有些副作用需要重建 // 存在 rebuilder 则表明有些副作用需要重建

View File

@ -6,6 +6,7 @@ import { execScripts } from 'import-html-entry';
import { isFunction } from 'lodash'; import { isFunction } from 'lodash';
import { frameworkConfiguration } from '../../../apis'; import { frameworkConfiguration } from '../../../apis';
import { qiankunHeadTagName } from '../../../utils'; import { qiankunHeadTagName } from '../../../utils';
import { lexicalGlobals } from '../../common';
import * as css from '../css'; import * as css from '../css';
export const rawHeadAppendChild = HTMLHeadElement.prototype.appendChild; export const rawHeadAppendChild = HTMLHeadElement.prototype.appendChild;
@ -183,6 +184,7 @@ export type ContainerConfig = {
appName: string; appName: string;
proxy: WindowProxy; proxy: WindowProxy;
strictGlobal: boolean; strictGlobal: boolean;
speedySandbox: boolean;
dynamicStyleSheetElements: Array<HTMLStyleElement | HTMLLinkElement>; dynamicStyleSheetElements: Array<HTMLStyleElement | HTMLLinkElement>;
appWrapperGetter: CallableFunction; appWrapperGetter: CallableFunction;
scopedCSS: boolean; scopedCSS: boolean;
@ -213,6 +215,7 @@ function getOverwrittenAppendChildOrInsertBefore(opts: {
appWrapperGetter, appWrapperGetter,
proxy, proxy,
strictGlobal, strictGlobal,
speedySandbox,
dynamicStyleSheetElements, dynamicStyleSheetElements,
scopedCSS, scopedCSS,
excludeAssetFilter, excludeAssetFilter,
@ -277,10 +280,13 @@ function getOverwrittenAppendChildOrInsertBefore(opts: {
const { fetch } = frameworkConfiguration; const { fetch } = frameworkConfiguration;
const referenceNode = mountDOM.contains(refChild) ? refChild : null; const referenceNode = mountDOM.contains(refChild) ? refChild : null;
const scopedGlobalVariables = speedySandbox ? lexicalGlobals : [];
if (src) { if (src) {
execScripts(null, [src], proxy, { execScripts(null, [src], proxy, {
fetch, fetch,
strictGlobal, strictGlobal,
scopedGlobalVariables,
beforeExec: () => { beforeExec: () => {
const isCurrentScriptConfigurable = () => { const isCurrentScriptConfigurable = () => {
const descriptor = Object.getOwnPropertyDescriptor(document, 'currentScript'); const descriptor = Object.getOwnPropertyDescriptor(document, 'currentScript');
@ -311,7 +317,7 @@ function getOverwrittenAppendChildOrInsertBefore(opts: {
} }
// inline script never trigger the onload and onerror event // inline script never trigger the onload and onerror event
execScripts(null, [`<script>${text}</script>`], proxy, { strictGlobal }); execScripts(null, [`<script>${text}</script>`], proxy, { strictGlobal, scopedGlobalVariables });
const dynamicInlineScriptCommentElement = document.createComment('dynamic inline script replaced by qiankun'); const dynamicInlineScriptCommentElement = document.createComment('dynamic inline script replaced by qiankun');
dynamicScriptAttachedCommentMap.set(element, dynamicInlineScriptCommentElement); dynamicScriptAttachedCommentMap.set(element, dynamicInlineScriptCommentElement);
return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicInlineScriptCommentElement, referenceNode); return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicInlineScriptCommentElement, referenceNode);

View File

@ -49,6 +49,7 @@ export function patchLooseSandbox(
appWrapperGetter, appWrapperGetter,
proxy, proxy,
strictGlobal: false, strictGlobal: false,
speedySandbox: false,
scopedCSS, scopedCSS,
dynamicStyleSheetElements, dynamicStyleSheetElements,
excludeAssetFilter, excludeAssetFilter,

View File

@ -19,19 +19,14 @@ import {
styleElementTargetSymbol, styleElementTargetSymbol,
} from './common'; } from './common';
declare global {
interface Window {
__proxyAttachContainerConfigMap__: WeakMap<WindowProxy, ContainerConfig>;
}
}
// Get native global window with a sandbox disgusted way, thus we could share it between qiankun instances🤪 // Get native global window with a sandbox disgusted way, thus we could share it between qiankun instances🤪
Object.defineProperty(nativeGlobal, '__proxyAttachContainerConfigMap__', { enumerable: false, writable: true }); Object.defineProperty(nativeGlobal, '__proxyAttachContainerConfigMap__', { enumerable: false, writable: true });
// Share proxyAttachContainerConfigMap between multiple qiankun instance, thus they could access the same record // Share proxyAttachContainerConfigMap between multiple qiankun instance, thus they could access the same record
nativeGlobal.__proxyAttachContainerConfigMap__ = nativeGlobal.__proxyAttachContainerConfigMap__ =
nativeGlobal.__proxyAttachContainerConfigMap__ || new WeakMap<WindowProxy, ContainerConfig>(); nativeGlobal.__proxyAttachContainerConfigMap__ || new WeakMap<WindowProxy, ContainerConfig>();
const proxyAttachContainerConfigMap = nativeGlobal.__proxyAttachContainerConfigMap__; const proxyAttachContainerConfigMap: WeakMap<WindowProxy, ContainerConfig> =
nativeGlobal.__proxyAttachContainerConfigMap__;
const elementAttachContainerConfigMap = new WeakMap<HTMLElement, ContainerConfig>(); const elementAttachContainerConfigMap = new WeakMap<HTMLElement, ContainerConfig>();
@ -83,6 +78,7 @@ export function patchStrictSandbox(
mounting = true, mounting = true,
scopedCSS = false, scopedCSS = false,
excludeAssetFilter?: CallableFunction, excludeAssetFilter?: CallableFunction,
speedySandbox = false,
): Freer { ): Freer {
let containerConfig = proxyAttachContainerConfigMap.get(proxy); let containerConfig = proxyAttachContainerConfigMap.get(proxy);
if (!containerConfig) { if (!containerConfig) {
@ -92,6 +88,7 @@ export function patchStrictSandbox(
appWrapperGetter, appWrapperGetter,
dynamicStyleSheetElements: [], dynamicStyleSheetElements: [],
strictGlobal: true, strictGlobal: true,
speedySandbox,
excludeAssetFilter, excludeAssetFilter,
scopedCSS, scopedCSS,
}; };

View File

@ -17,6 +17,7 @@ export function patchAtMounting(
sandbox: SandBox, sandbox: SandBox,
scopedCSS: boolean, scopedCSS: boolean,
excludeAssetFilter?: CallableFunction, excludeAssetFilter?: CallableFunction,
speedySandBox?: boolean,
): Freer[] { ): Freer[] {
const basePatchers = [ const basePatchers = [
() => patchInterval(sandbox.proxy), () => patchInterval(sandbox.proxy),
@ -31,7 +32,8 @@ export function patchAtMounting(
], ],
[SandBoxType.Proxy]: [ [SandBoxType.Proxy]: [
...basePatchers, ...basePatchers,
() => patchStrictSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter), () =>
patchStrictSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter, speedySandBox),
], ],
[SandBoxType.Snapshot]: [ [SandBoxType.Snapshot]: [
...basePatchers, ...basePatchers,
@ -48,13 +50,15 @@ export function patchAtBootstrapping(
sandbox: SandBox, sandbox: SandBox,
scopedCSS: boolean, scopedCSS: boolean,
excludeAssetFilter?: CallableFunction, excludeAssetFilter?: CallableFunction,
speedySandBox?: boolean,
): Freer[] { ): Freer[] {
const patchersInSandbox = { const patchersInSandbox = {
[SandBoxType.LegacyProxy]: [ [SandBoxType.LegacyProxy]: [
() => patchLooseSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter), () => patchLooseSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter),
], ],
[SandBoxType.Proxy]: [ [SandBoxType.Proxy]: [
() => patchStrictSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter), () =>
patchStrictSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter, speedySandBox),
], ],
[SandBoxType.Snapshot]: [ [SandBoxType.Snapshot]: [
() => patchLooseSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter), () => patchLooseSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter),

View File

@ -6,7 +6,7 @@
import type { SandBox } from '../interfaces'; import type { SandBox } from '../interfaces';
import { SandBoxType } from '../interfaces'; import { SandBoxType } from '../interfaces';
import { nativeGlobal, nextTask } from '../utils'; import { nativeGlobal, nextTask } from '../utils';
import { getCurrentRunningApp, getTargetValue, setCurrentRunningApp } from './common'; import { getCurrentRunningApp, getTargetValue, setCurrentRunningApp, unscopedGlobals } from './common';
type SymbolTarget = 'target' | 'globalContext'; type SymbolTarget = 'target' | 'globalContext';
@ -46,32 +46,10 @@ const variableWhiteList: PropertyKey[] = [
]; ];
/* /*
variables who are impossible to be overwrite need to be escaped from proxy sandbox for performance reasons variables who are impossible to be overwritten need to be escaped from proxy sandbox for performance reasons
see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/unscopables
*/ */
const unscopables = { const unscopables = unscopedGlobals.reduce((acc, key) => ({ ...acc, [key]: true }), { __proto__: null });
undefined: true,
Array: true,
Object: true,
String: true,
Boolean: true,
Math: true,
Number: true,
Symbol: true,
parseFloat: true,
Float32Array: true,
isNaN: true,
Infinity: true,
Reflect: true,
Float64Array: true,
Function: true,
Map: true,
NaN: true,
Promise: true,
Proxy: true,
Set: true,
parseInt: true,
requestAnimationFrame: true,
};
const useNativeWindowForBindingsProps = new Map<PropertyKey, boolean>([ const useNativeWindowForBindingsProps = new Map<PropertyKey, boolean>([
['fetch', true], ['fetch', true],
@ -151,28 +129,9 @@ export default class ProxySandbox implements SandBox {
type: SandBoxType; type: SandBoxType;
proxy: WindowProxy; proxy: WindowProxy;
globalContext: typeof window;
sandboxRunning = true; sandboxRunning = true;
latestSetProp: PropertyKey | null = null; latestSetProp: PropertyKey | null = null;
private registerRunningApp(name: string, proxy: Window) {
if (this.sandboxRunning) {
const currentRunningApp = getCurrentRunningApp();
if (!currentRunningApp || currentRunningApp.name !== name) {
setCurrentRunningApp({ name, window: proxy });
}
// FIXME if you have any other good ideas
// remove the mark in next tick, thus we can identify whether it in micro app or not
// this approach is just a workaround, it could not cover all complex cases, such as the micro app runs in the same task context with master in some case
nextTask(() => {
setCurrentRunningApp(null);
});
}
}
active() { active() {
if (!this.sandboxRunning) activeSandboxCount++; if (!this.sandboxRunning) activeSandboxCount++;
this.sandboxRunning = true; this.sandboxRunning = true;
@ -197,6 +156,8 @@ export default class ProxySandbox implements SandBox {
this.sandboxRunning = false; this.sandboxRunning = false;
} }
globalContext: typeof window;
constructor(name: string, globalContext = window) { constructor(name: string, globalContext = window) {
this.name = name; this.name = name;
this.globalContext = globalContext; this.globalContext = globalContext;
@ -377,4 +338,19 @@ export default class ProxySandbox implements SandBox {
activeSandboxCount++; activeSandboxCount++;
} }
private registerRunningApp(name: string, proxy: Window) {
if (this.sandboxRunning) {
const currentRunningApp = getCurrentRunningApp();
if (!currentRunningApp || currentRunningApp.name !== name) {
setCurrentRunningApp({ name, window: proxy });
}
// FIXME if you have any other good ideas
// remove the mark in next tick, thus we can identify whether it in micro app or not
// this approach is just a workaround, it could not cover all complex cases, such as the micro app runs in the same task context with master in some case
nextTask(() => {
setCurrentRunningApp(null);
});
}
}
} }