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:
parent
ea30a9d4f2
commit
5b1e39ef6e
6 changed files with 86 additions and 36 deletions
5
.changeset/wild-jobs-tan.md
Normal file
5
.changeset/wild-jobs-tan.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Move hoisted script analysis optimization behind the `experimental.optimizeHoistedScript` option
|
|
@ -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
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue