如何使用Webpack实现Gmail的JavaScript解析延迟策略?
Great question! That old Gmail trick of hiding deferred JS in HTML comments is a neat way to avoid initial parsing overhead, and it's totally replicable with Webpack. Here's a step-by-step breakdown of how to do it:
First, let's recap the core idea: We'll package our non-critical JS into a separate chunk, embed that chunk's code as an HTML comment (so browsers ignore it initially), and write a client-side function to extract and execute that code only when needed.
Step 1: Configure Webpack for Deferred Chunks
First, set up your Webpack config to split off the code you want to defer into its own chunk. We'll use html-webpack-plugin to generate our HTML, but we'll exclude the deferred chunk from automatic script injection.
// webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin'); const DeferredChunkPlugin = require('./deferred-chunk-plugin'); module.exports = { entry: { main: './src/main.js', // Your core app code deferredFeature: './src/deferred-feature.js' // Code to defer }, output: { filename: '[name].bundle.js', clean: true }, plugins: [ new HtmlWebpackPlugin({ template: './src/index.html', excludeChunks: ['deferredFeature'] // Don't inject as a script tag }), new DeferredChunkPlugin({ chunkName: 'deferredFeature' }) ] };
Step 2: Create a Custom Webpack Plugin to Embed Chunk as Comment
We need a plugin that takes our deferred chunk's code, escapes characters that would break HTML comments, and inserts it into our final HTML file wrapped in identifiable comments (so we can find it later).
// deferred-chunk-plugin.js class DeferredChunkPlugin { constructor(options) { this.chunkName = options.chunkName; } apply(compiler) { compiler.hooks.compilation.tap('DeferredChunkPlugin', (compilation) => { const HtmlWebpackPluginHooks = require('html-webpack-plugin').getHooks(compilation); HtmlWebpackPluginHooks.afterTemplateExecution.tapAsync( 'DeferredChunkPlugin', (data, callback) => { // Locate our deferred chunk const targetChunk = compilation.chunks.find(chunk => chunk.name === this.chunkName); if (!targetChunk) { callback(null, data); return; } // Get the chunk's source code const assetFilename = targetChunk.files[0]; const rawCode = compilation.assets[assetFilename].source(); // Escape characters that would break HTML comment syntax const escapedCode = rawCode .replace(/-->/g, '--\\>') .replace(/<!--/g, '\\<!--'); // Wrap code in identifiable comments for later retrieval const deferredCommentBlock = ` <!-- deferred-chunk: ${this.chunkName} --> ${escapedCode} <!-- /deferred-chunk: ${this.chunkName} --> `.trim(); // Insert the comment block right before the closing </body> tag data.html = data.html.replace('</body>', `${deferredCommentBlock}\n</body>`); callback(null, data); } ); }); } } module.exports = DeferredChunkPlugin;
Step 3: Write a Client-Side Loader Function
Now we need a function to find the comment block, extract the code, and execute it when needed. This function will traverse the DOM's comment nodes, locate our target chunk, and run the code (using eval, just like Gmail's approach).
// src/main.js async function loadDeferredChunk(chunkName) { return new Promise((resolve, reject) => { // Traverse all comment nodes in the body const commentIterator = document.createNodeIterator( document.body, Node.COMMENT_NODE ); let currentComment; let chunkCode = ''; let isTargetChunk = false; while ((currentComment = commentIterator.nextNode())) { const commentText = currentComment.textContent.trim(); // Start extracting when we hit the opening chunk marker if (commentText === `deferred-chunk: ${chunkName}`) { isTargetChunk = true; let nextNode = currentComment.nextSibling; // Collect all content until we hit the closing marker while (nextNode && nextNode.nodeType !== Node.COMMENT_NODE) { chunkCode += nextNode.textContent || ''; nextNode = nextNode.nextSibling; } // Validate we found the closing marker if (nextNode?.textContent.trim() === `/deferred-chunk: ${chunkName}`) { break; } else { reject(new Error(`Invalid structure for deferred chunk: ${chunkName}`)); return; } } } if (!chunkCode) { reject(new Error(`Deferred chunk "${chunkName}" not found`)); return; } try { // Create a module-like environment for Webpack's bundled code const module = { exports: {} }; // If your deferred code has dependencies, you'll need to handle `require` here (simplified for example) const require = (dep) => { throw new Error('Dependency handling not implemented in this example'); }; // Execute the deferred code eval(chunkCode); // Resolve with the exported values from the deferred module resolve(module.exports); } catch (error) { reject(new Error(`Failed to execute deferred chunk: ${error.message}`)); } }); } // Example usage: Load the deferred code when a button is clicked document.getElementById('load-feature-btn').addEventListener('click', async () => { try { const deferredFeature = await loadDeferredChunk('deferredFeature'); deferredFeature.run(); // Call a function from your deferred code } catch (err) { console.error('Error loading deferred feature:', err); } });
Step 4: Example Deferred Code
Your deferred code can be a standard ES module—Webpack will bundle it properly, and our loader function will access its exports.
// src/deferred-feature.js export function run() { console.log('Deferred feature loaded and executed!'); // Add your non-critical functionality here }
Key Notes
- Security: Using
evalcarries inherent risks, so only use this approach with code you fully trust (just like Gmail did). - Dependencies: If your deferred code relies on other modules, you'll need to either bundle those dependencies into the same chunk or extend the
requirefunction in the loader to handle them. - Performance: This avoids initial parsing of non-critical JS, which can improve LCP and other core web vitals for large apps.
内容的提问来源于stack exchange,提问作者Abhinav Singi

