Skip to content

Commit

Permalink
Move hoisted script analysis optimization as experimental (#8011)
Browse files Browse the repository at this point in the history
Co-authored-by: Sarah Rainsberger <[email protected]>
  • Loading branch information
bluwy and sarah11918 authored Aug 10, 2023
1 parent ea30a9d commit 5b1e39e
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 38 deletions.
5 changes: 5 additions & 0 deletions .changeset/wild-jobs-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Move hoisted script analysis optimization behind the `experimental.optimizeHoistedScript` option
22 changes: 22 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1272,6 +1272,28 @@ export interface AstroUserConfig {
* ```
*/
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
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/build/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { pluginSSR, pluginSSRSplit } from './plugin-ssr.js';
export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) {
register(pluginComponentEntry(internals));
register(pluginAliasResolve(internals));
register(pluginAnalyzer(internals));
register(pluginAnalyzer(options, internals));
register(pluginInternals(internals));
register(pluginRenderers(options));
register(pluginMiddleware(options, internals));
Expand Down
92 changes: 55 additions & 37 deletions packages/astro/src/core/build/plugins/plugin-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import type { BuildInternals } from '../internal.js';
import type { AstroBuildPlugin } from '../plugin.js';

import type { ExportDefaultDeclaration, ExportNamedDeclaration, ImportDeclaration } from 'estree';
import { walk } from 'estree-walker';
import { PROPAGATED_ASSET_FLAG } from '../../../content/consts.js';
import { prependForwardSlash } from '../../../core/path.js';
import { getTopLevelPages, moduleIsTopLevelPage, walkParentInfos } from '../graph.js';
import type { StaticBuildOptions } from '../types.js';
import { getPageDataByViteID, trackClientOnlyPageDatas } from '../internal.js';

function isPropagatedAsset(id: string) {
Expand All @@ -30,29 +30,28 @@ async function doesParentImportChild(
): Promise<'no' | 'dynamic' | string[]> {
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)) {
return 'dynamic';
}

const imports: Array<ImportDeclaration> = [];
const exports: Array<ExportNamedDeclaration | ExportDefaultDeclaration> = [];
walk(parentInfo.ast, {
enter(node) {
if (node.type === 'ImportDeclaration') {
imports.push(node as ImportDeclaration);
} else if (
node.type === 'ExportDefaultDeclaration' ||
node.type === 'ExportNamedDeclaration'
) {
exports.push(node as ExportNamedDeclaration | ExportDefaultDeclaration);
}
},
});
// All of the aliases the current component is imported as
for (const node of (parentInfo.ast as any).body) {
if (node.type === 'ImportDeclaration') {
imports.push(node);
} else if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportNamedDeclaration') {
exports.push(node);
}
}

// All local import names that could be importing the child component
const importNames: string[] = [];
// All of the aliases the child component is exported as
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) {
const resolved = await this.resolve(node.source.value as string, parentInfo.id);
if (!resolved || resolved.id !== childInfo.id) continue;
Expand All @@ -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) {
if (node.type === 'ExportDefaultDeclaration') {
if (node.declaration.type === 'Identifier' && importNames.includes(node.declaration.name)) {
exportNames.push('default');
// return
}
} 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) {
const resolved = await this.resolve(node.source.value as string, parentInfo.id);
if (!resolved || resolved.id !== childInfo.id) continue;
Expand All @@ -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.type !== 'VariableDeclaration') continue;
for (const declarator of node.declaration.declarations) {
Expand All @@ -95,6 +100,9 @@ async function doesParentImportChild(
}
}
}
// Handle:
// export { Component }
// export { Component as AliasedComponent }
for (const specifier of node.specifiers) {
if (importNames.includes(specifier.local.name)) {
exportNames.push(specifier.exported.name);
Expand All @@ -115,7 +123,10 @@ async function doesParentImportChild(
return exportNames;
}

export function vitePluginAnalyzer(internals: BuildInternals): VitePlugin {
export function vitePluginAnalyzer(
options: StaticBuildOptions,
internals: BuildInternals
): VitePlugin {
function hoistedScriptScanner() {
const uniqueHoistedIds = new Map<string, string>();
const pageScripts = new Map<
Expand All @@ -139,6 +150,7 @@ export function vitePluginAnalyzer(internals: BuildInternals): VitePlugin {
}

if (hoistedScripts.size) {
// These variables are only used for hoisted script analysis optimization
const depthsToChildren = new Map<number, ModuleInfo>();
const depthsToExportNames = new Map<number, string[] | 'dynamic'>();
// The component export from the original component file will always be default.
Expand All @@ -147,25 +159,28 @@ export function vitePluginAnalyzer(internals: BuildInternals): VitePlugin {
for (const [parentInfo, depth] of walkParentInfos(from, this, function until(importer) {
return isPropagatedAsset(importer);
})) {
depthsToChildren.set(depth, parentInfo);
// If at any point
if (depth > 0) {
// Check if the component is actually imported:
const childInfo = depthsToChildren.get(depth - 1);
const childExportNames = depthsToExportNames.get(depth - 1);

const doesImport = await doesParentImportChild.call(
this,
parentInfo,
childInfo,
childExportNames
);

if (doesImport === 'no') {
// Break the search if the parent doesn't import the child.
continue;
// If hoisted script analysis optimization is enabled, try to analyse and bail early if possible
if (options.settings.config.experimental.optimizeHoistedScript) {
depthsToChildren.set(depth, parentInfo);
// If at any point
if (depth > 0) {
// Check if the component is actually imported:
const childInfo = depthsToChildren.get(depth - 1);
const childExportNames = depthsToExportNames.get(depth - 1);

const doesImport = await doesParentImportChild.call(
this,
parentInfo,
childInfo,
childExportNames
);

if (doesImport === 'no') {
// Break the search if the parent doesn't import the child.
continue;
}
depthsToExportNames.set(depth, doesImport);
}
depthsToExportNames.set(depth, doesImport);
}

if (isPropagatedAsset(parentInfo.id)) {
Expand Down Expand Up @@ -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 {
build: 'ssr',
hooks: {
'build:before': () => {
return {
vitePlugin: vitePluginAnalyzer(internals),
vitePlugin: vitePluginAnalyzer(options, internals),
};
},
},
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const ASTRO_CONFIG_DEFAULTS = {
experimental: {
assets: false,
viewTransitions: false,
optimizeHoistedScript: false
},
} satisfies AstroUserConfig & { server: { open: boolean } };

Expand Down Expand Up @@ -237,6 +238,7 @@ export const AstroConfigSchema = z.object({
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.viewTransitions),
optimizeHoistedScript: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.optimizeHoistedScript),
})
.passthrough()
.refine(
Expand Down
3 changes: 3 additions & 0 deletions packages/astro/test/hoisted-imports.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ describe('Hoisted Imports', () => {
before(async () => {
fixture = await loadFixture({
root: './fixtures/hoisted-imports/',
experimental: {
optimizeHoistedScript: true,
},
});
});

Expand Down

0 comments on commit 5b1e39e

Please sign in to comment.