0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-01-20 22:12:38 -05:00

Move hoisted script analysis optimization as experimental (#8011)

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
Bjorn Lu 2023-08-10 12:52:57 +08:00 committed by GitHub
parent ea30a9d4f2
commit 5b1e39ef6e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 86 additions and 36 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Move hoisted script analysis optimization behind the `experimental.optimizeHoistedScript` option

View file

@ -1272,6 +1272,28 @@ export interface AstroUserConfig {
* ``` * ```
*/ */
viewTransitions?: boolean; viewTransitions?: boolean;
/**
* @docs
* @name experimental.optimizeHoistedScript
* @type {boolean}
* @default `false`
* @version 2.10.4
* @description
* Prevents unused components' scripts from being included in a page unexpectedly.
* The optimization is best-effort and may inversely miss including the used scripts. Make sure to double-check your built pages
* before publishing.
* Enable hoisted script analysis optimization by adding the experimental flag:
*
* ```js
* {
* experimental: {
* optimizeHoistedScript: true,
* },
* }
* ```
*/
optimizeHoistedScript?: boolean;
}; };
// Legacy options to be removed // Legacy options to be removed

View file

@ -16,7 +16,7 @@ import { pluginSSR, pluginSSRSplit } from './plugin-ssr.js';
export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) { export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) {
register(pluginComponentEntry(internals)); register(pluginComponentEntry(internals));
register(pluginAliasResolve(internals)); register(pluginAliasResolve(internals));
register(pluginAnalyzer(internals)); register(pluginAnalyzer(options, internals));
register(pluginInternals(internals)); register(pluginInternals(internals));
register(pluginRenderers(options)); register(pluginRenderers(options));
register(pluginMiddleware(options, internals)); register(pluginMiddleware(options, internals));

View file

@ -5,10 +5,10 @@ import type { BuildInternals } from '../internal.js';
import type { AstroBuildPlugin } from '../plugin.js'; import type { AstroBuildPlugin } from '../plugin.js';
import type { ExportDefaultDeclaration, ExportNamedDeclaration, ImportDeclaration } from 'estree'; import type { ExportDefaultDeclaration, ExportNamedDeclaration, ImportDeclaration } from 'estree';
import { walk } from 'estree-walker';
import { PROPAGATED_ASSET_FLAG } from '../../../content/consts.js'; import { PROPAGATED_ASSET_FLAG } from '../../../content/consts.js';
import { prependForwardSlash } from '../../../core/path.js'; import { prependForwardSlash } from '../../../core/path.js';
import { getTopLevelPages, moduleIsTopLevelPage, walkParentInfos } from '../graph.js'; import { getTopLevelPages, moduleIsTopLevelPage, walkParentInfos } from '../graph.js';
import type { StaticBuildOptions } from '../types.js';
import { getPageDataByViteID, trackClientOnlyPageDatas } from '../internal.js'; import { getPageDataByViteID, trackClientOnlyPageDatas } from '../internal.js';
function isPropagatedAsset(id: string) { function isPropagatedAsset(id: string) {
@ -30,29 +30,28 @@ async function doesParentImportChild(
): Promise<'no' | 'dynamic' | string[]> { ): Promise<'no' | 'dynamic' | string[]> {
if (!childInfo || !parentInfo.ast || !childExportNames) return 'no'; if (!childInfo || !parentInfo.ast || !childExportNames) return 'no';
// If we're dynamically importing the child, return `dynamic` directly to opt-out of optimization
if (childExportNames === 'dynamic' || parentInfo.dynamicallyImportedIds?.includes(childInfo.id)) { if (childExportNames === 'dynamic' || parentInfo.dynamicallyImportedIds?.includes(childInfo.id)) {
return 'dynamic'; return 'dynamic';
} }
const imports: Array<ImportDeclaration> = []; const imports: Array<ImportDeclaration> = [];
const exports: Array<ExportNamedDeclaration | ExportDefaultDeclaration> = []; const exports: Array<ExportNamedDeclaration | ExportDefaultDeclaration> = [];
walk(parentInfo.ast, { for (const node of (parentInfo.ast as any).body) {
enter(node) { if (node.type === 'ImportDeclaration') {
if (node.type === 'ImportDeclaration') { imports.push(node);
imports.push(node as ImportDeclaration); } else if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportNamedDeclaration') {
} else if ( exports.push(node);
node.type === 'ExportDefaultDeclaration' || }
node.type === 'ExportNamedDeclaration' }
) {
exports.push(node as ExportNamedDeclaration | ExportDefaultDeclaration); // All local import names that could be importing the child component
}
},
});
// All of the aliases the current component is imported as
const importNames: string[] = []; const importNames: string[] = [];
// All of the aliases the child component is exported as // All of the aliases the child component is exported as
const exportNames: string[] = []; const exportNames: string[] = [];
// Iterate each import, find it they import the child component, if so, check if they
// import the child component name specifically. We can verify this with `childExportNames`.
for (const node of imports) { for (const node of imports) {
const resolved = await this.resolve(node.source.value as string, parentInfo.id); const resolved = await this.resolve(node.source.value as string, parentInfo.id);
if (!resolved || resolved.id !== childInfo.id) continue; if (!resolved || resolved.id !== childInfo.id) continue;
@ -67,14 +66,17 @@ async function doesParentImportChild(
} }
} }
} }
// Iterate each export, find it they re-export the child component, and push the exported name to `exportNames`
for (const node of exports) { for (const node of exports) {
if (node.type === 'ExportDefaultDeclaration') { if (node.type === 'ExportDefaultDeclaration') {
if (node.declaration.type === 'Identifier' && importNames.includes(node.declaration.name)) { if (node.declaration.type === 'Identifier' && importNames.includes(node.declaration.name)) {
exportNames.push('default'); exportNames.push('default');
// return
} }
} else { } else {
// handle `export { x } from 'something';`, where the export and import are in the same node // Handle:
// export { Component } from './Component.astro'
// export { Component as AliasedComponent } from './Component.astro'
if (node.source) { if (node.source) {
const resolved = await this.resolve(node.source.value as string, parentInfo.id); const resolved = await this.resolve(node.source.value as string, parentInfo.id);
if (!resolved || resolved.id !== childInfo.id) continue; if (!resolved || resolved.id !== childInfo.id) continue;
@ -85,6 +87,9 @@ async function doesParentImportChild(
} }
} }
} }
// Handle:
// export const AliasedComponent = Component
// export const AliasedComponent = Component, let foo = 'bar'
if (node.declaration) { if (node.declaration) {
if (node.declaration.type !== 'VariableDeclaration') continue; if (node.declaration.type !== 'VariableDeclaration') continue;
for (const declarator of node.declaration.declarations) { for (const declarator of node.declaration.declarations) {
@ -95,6 +100,9 @@ async function doesParentImportChild(
} }
} }
} }
// Handle:
// export { Component }
// export { Component as AliasedComponent }
for (const specifier of node.specifiers) { for (const specifier of node.specifiers) {
if (importNames.includes(specifier.local.name)) { if (importNames.includes(specifier.local.name)) {
exportNames.push(specifier.exported.name); exportNames.push(specifier.exported.name);
@ -115,7 +123,10 @@ async function doesParentImportChild(
return exportNames; return exportNames;
} }
export function vitePluginAnalyzer(internals: BuildInternals): VitePlugin { export function vitePluginAnalyzer(
options: StaticBuildOptions,
internals: BuildInternals
): VitePlugin {
function hoistedScriptScanner() { function hoistedScriptScanner() {
const uniqueHoistedIds = new Map<string, string>(); const uniqueHoistedIds = new Map<string, string>();
const pageScripts = new Map< const pageScripts = new Map<
@ -139,6 +150,7 @@ export function vitePluginAnalyzer(internals: BuildInternals): VitePlugin {
} }
if (hoistedScripts.size) { if (hoistedScripts.size) {
// These variables are only used for hoisted script analysis optimization
const depthsToChildren = new Map<number, ModuleInfo>(); const depthsToChildren = new Map<number, ModuleInfo>();
const depthsToExportNames = new Map<number, string[] | 'dynamic'>(); const depthsToExportNames = new Map<number, string[] | 'dynamic'>();
// The component export from the original component file will always be default. // The component export from the original component file will always be default.
@ -147,25 +159,28 @@ export function vitePluginAnalyzer(internals: BuildInternals): VitePlugin {
for (const [parentInfo, depth] of walkParentInfos(from, this, function until(importer) { for (const [parentInfo, depth] of walkParentInfos(from, this, function until(importer) {
return isPropagatedAsset(importer); return isPropagatedAsset(importer);
})) { })) {
depthsToChildren.set(depth, parentInfo); // If hoisted script analysis optimization is enabled, try to analyse and bail early if possible
// If at any point if (options.settings.config.experimental.optimizeHoistedScript) {
if (depth > 0) { depthsToChildren.set(depth, parentInfo);
// Check if the component is actually imported: // If at any point
const childInfo = depthsToChildren.get(depth - 1); if (depth > 0) {
const childExportNames = depthsToExportNames.get(depth - 1); // Check if the component is actually imported:
const childInfo = depthsToChildren.get(depth - 1);
const childExportNames = depthsToExportNames.get(depth - 1);
const doesImport = await doesParentImportChild.call( const doesImport = await doesParentImportChild.call(
this, this,
parentInfo, parentInfo,
childInfo, childInfo,
childExportNames childExportNames
); );
if (doesImport === 'no') { if (doesImport === 'no') {
// Break the search if the parent doesn't import the child. // Break the search if the parent doesn't import the child.
continue; continue;
}
depthsToExportNames.set(depth, doesImport);
} }
depthsToExportNames.set(depth, doesImport);
} }
if (isPropagatedAsset(parentInfo.id)) { if (isPropagatedAsset(parentInfo.id)) {
@ -310,13 +325,16 @@ export function vitePluginAnalyzer(internals: BuildInternals): VitePlugin {
}; };
} }
export function pluginAnalyzer(internals: BuildInternals): AstroBuildPlugin { export function pluginAnalyzer(
options: StaticBuildOptions,
internals: BuildInternals
): AstroBuildPlugin {
return { return {
build: 'ssr', build: 'ssr',
hooks: { hooks: {
'build:before': () => { 'build:before': () => {
return { return {
vitePlugin: vitePluginAnalyzer(internals), vitePlugin: vitePluginAnalyzer(options, internals),
}; };
}, },
}, },

View file

@ -46,6 +46,7 @@ const ASTRO_CONFIG_DEFAULTS = {
experimental: { experimental: {
assets: false, assets: false,
viewTransitions: false, viewTransitions: false,
optimizeHoistedScript: false
}, },
} satisfies AstroUserConfig & { server: { open: boolean } }; } satisfies AstroUserConfig & { server: { open: boolean } };
@ -237,6 +238,7 @@ export const AstroConfigSchema = z.object({
.boolean() .boolean()
.optional() .optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.viewTransitions), .default(ASTRO_CONFIG_DEFAULTS.experimental.viewTransitions),
optimizeHoistedScript: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.optimizeHoistedScript),
}) })
.passthrough() .passthrough()
.refine( .refine(

View file

@ -8,6 +8,9 @@ describe('Hoisted Imports', () => {
before(async () => { before(async () => {
fixture = await loadFixture({ fixture = await loadFixture({
root: './fixtures/hoisted-imports/', root: './fixtures/hoisted-imports/',
experimental: {
optimizeHoistedScript: true,
},
}); });
}); });