From e687d9886fd351d9941fc0a1a4c754440a94335f Mon Sep 17 00:00:00 2001 From: Andy Li Date: Tue, 14 Sep 2021 06:10:16 +0800 Subject: [PATCH] feat(html): Inline entry chunk when possible (#4555) --- .../playground/html/__tests__/html.spec.ts | 34 ++++++++ packages/playground/html/inline/common.js | 8 ++ packages/playground/html/inline/dep1.js | 3 + packages/playground/html/inline/dep2.js | 3 + packages/playground/html/inline/dep3.js | 4 + .../playground/html/inline/module-graph.dot | 16 ++++ packages/playground/html/inline/shared-1.html | 2 + packages/playground/html/inline/shared-2.html | 2 + packages/playground/html/inline/shared.js | 4 + packages/playground/html/inline/unique.html | 2 + packages/playground/html/inline/unique.js | 4 + packages/playground/html/vite.config.js | 5 +- packages/vite/src/node/plugins/html.ts | 82 +++++++++++++------ 13 files changed, 141 insertions(+), 28 deletions(-) create mode 100644 packages/playground/html/inline/common.js create mode 100644 packages/playground/html/inline/dep1.js create mode 100644 packages/playground/html/inline/dep2.js create mode 100644 packages/playground/html/inline/dep3.js create mode 100644 packages/playground/html/inline/module-graph.dot create mode 100644 packages/playground/html/inline/shared-1.html create mode 100644 packages/playground/html/inline/shared-2.html create mode 100644 packages/playground/html/inline/shared.js create mode 100644 packages/playground/html/inline/unique.html create mode 100644 packages/playground/html/inline/unique.js diff --git a/packages/playground/html/__tests__/html.spec.ts b/packages/playground/html/__tests__/html.spec.ts index ed1ae6e91ad71e..4d06ee7463cffd 100644 --- a/packages/playground/html/__tests__/html.spec.ts +++ b/packages/playground/html/__tests__/html.spec.ts @@ -94,3 +94,37 @@ describe('nested w/ query', () => { testPage(true) }) + +if (isBuild) { + describe('inline entry', () => { + const _countTags = (selector) => page.$$eval(selector, (t) => t.length) + const countScriptTags = _countTags.bind(this, 'script[type=module]') + const countPreloadTags = _countTags.bind(this, 'link[rel=modulepreload]') + + test('is inlined', async () => { + await page.goto(viteTestUrl + '/inline/shared-1.html?v=1') + expect(await countScriptTags()).toBeGreaterThan(1) + expect(await countPreloadTags()).toBe(0) + }) + + test('is not inlined', async () => { + await page.goto(viteTestUrl + '/inline/unique.html?v=1') + expect(await countScriptTags()).toBe(1) + expect(await countPreloadTags()).toBeGreaterThan(0) + }) + + test('execution order when inlined', async () => { + await page.goto(viteTestUrl + '/inline/shared-2.html?v=1') + expect((await page.textContent('#output')).trim()).toBe( + 'dep1 common dep2 dep3 shared' + ) + }) + + test('execution order when not inlined', async () => { + await page.goto(viteTestUrl + '/inline/unique.html?v=1') + expect((await page.textContent('#output')).trim()).toBe( + 'dep1 common dep2 unique' + ) + }) + }) +} diff --git a/packages/playground/html/inline/common.js b/packages/playground/html/inline/common.js new file mode 100644 index 00000000000000..d6f029f0558e6b --- /dev/null +++ b/packages/playground/html/inline/common.js @@ -0,0 +1,8 @@ +import './dep1' +import './dep2' + +export function log(name) { + document.getElementById('output').innerHTML += name + ' ' +} + +log('common') diff --git a/packages/playground/html/inline/dep1.js b/packages/playground/html/inline/dep1.js new file mode 100644 index 00000000000000..826758fcea26dd --- /dev/null +++ b/packages/playground/html/inline/dep1.js @@ -0,0 +1,3 @@ +import { log } from './common' + +log('dep1') diff --git a/packages/playground/html/inline/dep2.js b/packages/playground/html/inline/dep2.js new file mode 100644 index 00000000000000..8e1189ab87967d --- /dev/null +++ b/packages/playground/html/inline/dep2.js @@ -0,0 +1,3 @@ +import { log } from './common' + +log('dep2') diff --git a/packages/playground/html/inline/dep3.js b/packages/playground/html/inline/dep3.js new file mode 100644 index 00000000000000..fd73e87b5184d5 --- /dev/null +++ b/packages/playground/html/inline/dep3.js @@ -0,0 +1,4 @@ +import './dep2' +import { log } from './common' + +log('dep3') diff --git a/packages/playground/html/inline/module-graph.dot b/packages/playground/html/inline/module-graph.dot new file mode 100644 index 00000000000000..38e98c98b4e0f6 --- /dev/null +++ b/packages/playground/html/inline/module-graph.dot @@ -0,0 +1,16 @@ +digraph Module { + common -> { dep1, dep2 } [style=dashed,color=grey] + dep1 -> common + dep2 -> common + dep3 -> { dep2, common } + + subgraph shared { + shared [style=filled] + shared -> { dep3, common } + } + + subgraph unique { + unique [style=filled] + unique -> { common, dep2 } + } +} diff --git a/packages/playground/html/inline/shared-1.html b/packages/playground/html/inline/shared-1.html new file mode 100644 index 00000000000000..69e8b612a3f75d --- /dev/null +++ b/packages/playground/html/inline/shared-1.html @@ -0,0 +1,2 @@ +

+
diff --git a/packages/playground/html/inline/shared-2.html b/packages/playground/html/inline/shared-2.html
new file mode 100644
index 00000000000000..69e8b612a3f75d
--- /dev/null
+++ b/packages/playground/html/inline/shared-2.html
@@ -0,0 +1,2 @@
+

+
diff --git a/packages/playground/html/inline/shared.js b/packages/playground/html/inline/shared.js
new file mode 100644
index 00000000000000..531a2786543146
--- /dev/null
+++ b/packages/playground/html/inline/shared.js
@@ -0,0 +1,4 @@
+import './dep3'
+import { log } from './common'
+
+log('shared')
diff --git a/packages/playground/html/inline/unique.html b/packages/playground/html/inline/unique.html
new file mode 100644
index 00000000000000..96a7b67adf90d5
--- /dev/null
+++ b/packages/playground/html/inline/unique.html
@@ -0,0 +1,2 @@
+

+
diff --git a/packages/playground/html/inline/unique.js b/packages/playground/html/inline/unique.js
new file mode 100644
index 00000000000000..df15756c072268
--- /dev/null
+++ b/packages/playground/html/inline/unique.js
@@ -0,0 +1,4 @@
+import { log } from './common'
+import './dep2'
+
+log('unique')
diff --git a/packages/playground/html/vite.config.js b/packages/playground/html/vite.config.js
index ba56b2f2555bd9..61ee5107d819c4 100644
--- a/packages/playground/html/vite.config.js
+++ b/packages/playground/html/vite.config.js
@@ -8,7 +8,10 @@ module.exports = {
     rollupOptions: {
       input: {
         main: resolve(__dirname, 'index.html'),
-        nested: resolve(__dirname, 'nested/index.html')
+        nested: resolve(__dirname, 'nested/index.html'),
+        inline1: resolve(__dirname, 'inline/shared-1.html'),
+        inline2: resolve(__dirname, 'inline/shared-2.html'),
+        inline3: resolve(__dirname, 'inline/unique.html'),
       }
     }
   },
diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts
index 21f8bff0b406dc..8da6800cd03ba5 100644
--- a/packages/vite/src/node/plugins/html.ts
+++ b/packages/vite/src/node/plugins/html.ts
@@ -273,30 +273,43 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
       }
     },
 
-    async generateBundle(_, bundle) {
+    async generateBundle(options, bundle) {
       const analyzedChunk: Map = new Map()
-      const getPreloadLinksForChunk = (
+      const getImportedChunks = (
         chunk: OutputChunk,
         seen: Set = new Set()
-      ): HtmlTagDescriptor[] => {
-        const tags: HtmlTagDescriptor[] = []
+      ): OutputChunk[] => {
+        const chunks: OutputChunk[] = []
         chunk.imports.forEach((file) => {
           const importee = bundle[file]
           if (importee?.type === 'chunk' && !seen.has(file)) {
             seen.add(file)
-            tags.push({
-              tag: 'link',
-              attrs: {
-                rel: 'modulepreload',
-                href: toPublicPath(file, config)
-              }
-            })
-            tags.push(...getPreloadLinksForChunk(importee, seen))
+
+            // post-order traversal
+            chunks.push(...getImportedChunks(importee, seen))
+            chunks.push(importee)
           }
         })
-        return tags
+        return chunks
       }
 
+      const toScriptTag = (chunk: OutputChunk): HtmlTagDescriptor => ({
+        tag: 'script',
+        attrs: {
+          type: 'module',
+          crossorigin: true,
+          src: toPublicPath(chunk.fileName, config)
+        }
+      })
+
+      const toPreloadTag = (chunk: OutputChunk): HtmlTagDescriptor => ({
+        tag: 'link',
+        attrs: {
+          rel: 'modulepreload',
+          href: toPublicPath(chunk.fileName, config)
+        }
+      })
+
       const getCssTagsForChunk = (
         chunk: OutputChunk,
         seen: Set = new Set()
@@ -343,23 +356,25 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
             chunk.isEntry &&
             chunk.facadeModuleId === id
         ) as OutputChunk | undefined
+        let canInlineEntry = false
 
         // inject chunk asset links
         if (chunk) {
-          const assetTags = [
-            // js entry chunk for this page
-            {
-              tag: 'script',
-              attrs: {
-                type: 'module',
-                crossorigin: true,
-                src: toPublicPath(chunk.fileName, config)
-              }
-            },
-            // preload for imports
-            ...getPreloadLinksForChunk(chunk),
-            ...getCssTagsForChunk(chunk)
-          ]
+          // an entry chunk can be inlined if
+          //  - it's an ES module (e.g. not generated by the legacy plugin)
+          //  - it contains no meaningful code other than import statments
+          if (options.format === 'es' && isEntirelyImport(chunk.code)) {
+            canInlineEntry = true
+          }
+
+          // when not inlined, inject