perf(v2): reduce HTML payload by eliminating chunk-map (#2118)

* perf(v2): eliminate chunk-map bloat while preserving prefetch functionality

* review

* refactor
This commit is contained in:
Endi 2019-12-13 15:55:45 +07:00 committed by GitHub
parent 4f0ae6d117
commit daa71b2eff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 60 additions and 127 deletions

View File

@ -37,6 +37,8 @@ const docusaurus = {
if (!canPrefetch(routePath)) {
return false;
}
// prevent future duplicate prefetch of routePath
fetched[routePath] = true;
// Find all webpack chunk names needed
const matches = matchRoutes(routes, routePath);
@ -51,12 +53,14 @@ const docusaurus = {
}, []);
// Prefetch all webpack chunk assets file needed
const chunkAssetsNeeded = chunkNamesNeeded.reduce((arr, chunkName) => {
const chunkAssets = window.__chunkMapping[chunkName] || [];
return arr.concat(chunkAssets);
}, []);
Promise.all(chunkAssetsNeeded.map(prefetchHelper)).then(() => {
fetched[routePath] = true;
chunkNamesNeeded.forEach(chunkName => {
// "__webpack_require__.gca" is a custom function provided by ChunkAssetPlugin
// Pass it the chunkName or chunkId you want to load and it will return the URL for that chunk
// eslint-disable-next-line no-undef
const chunkAsset = __webpack_require__.gca(chunkName);
if (chunkAsset) {
prefetchHelper(chunkAsset);
}
});
return true;
},

View File

@ -50,18 +50,6 @@ export default async function render(locals) {
const manifestPath = path.join(generatedFilesDir, 'client-manifest.json');
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
// chunkName -> chunkAssets mapping.
const chunkManifestPath = path.join(generatedFilesDir, 'chunk-map.json');
const chunkManifest = JSON.parse(
await fs.readFile(chunkManifestPath, 'utf8'),
);
const chunkManifestScript =
`<script type="text/javascript">` +
`/*<![CDATA[*/window.__chunkMapping=${JSON.stringify(
chunkManifest,
)};/*]]>*/` +
`</script>`;
// Get all required assets for this particular page based on client manifest information
const modulesToBeLoaded = [...manifest.entrypoints, ...Array.from(modules)];
const bundles = getBundles(manifest, modulesToBeLoaded);
@ -74,7 +62,6 @@ export default async function render(locals) {
{
appHtml,
baseUrl,
chunkManifestScript,
htmlAttributes: htmlAttributes || '',
bodyAttributes: bodyAttributes || '',
headTags,

View File

@ -13,7 +13,6 @@ module.exports = `
<meta name="viewport" content="width=device-width">
<meta name="generator" content="Docusaurus">
<%- headTags %>
<%- chunkManifestScript %>
<% metaAttributes.forEach((metaAttribute) => { %>
<%- metaAttribute %>
<% }); %>

View File

@ -108,16 +108,10 @@ export async function build(
);
});
// Make sure generated client-manifest and chunk-map is cleaned first so we don't reuse the one from prevs build
const chunkManifestPath = path.join(generatedFilesDir, 'chunk-map.json');
await Promise.all(
[clientManifestPath, chunkManifestPath].map(async manifestPath => {
const manifestExist = await fs.pathExists(manifestPath);
if (manifestExist) {
await fs.unlink(manifestPath);
}
}),
);
// Make sure generated client-manifest is cleaned first so we don't reuse the one from prevs build
if (fs.existsSync(clientManifestPath)) {
fs.unlinkSync(clientManifestPath);
}
// Run webpack to build JS bundle (client) and static html files (server).
await compile([clientConfig, serverConfig]);

View File

@ -10,7 +10,7 @@ import merge from 'webpack-merge';
import {Props} from '@docusaurus/types';
import {createBaseConfig} from './base';
import ChunkManifestPlugin from './plugins/ChunkManifestPlugin';
import ChunkAssetPlugin from './plugins/ChunkAssetPlugin';
import LogPlugin from './plugins/LogPlugin';
export function createClientConfig(props: Props): Configuration {
@ -30,13 +30,7 @@ export function createClientConfig(props: Props): Configuration {
runtimeChunk: true,
},
plugins: [
// Generate chunk-map.json (mapping of chunk names to their corresponding chunk assets)
new ChunkManifestPlugin({
filename: 'chunk-map.json',
outputPath: props.generatedFilesDir,
manifestVariable: '__chunkMapping',
inlineManifest: !isProd,
}),
new ChunkAssetPlugin(),
// Show compilation progress bar and build time.
new LogPlugin({
name: 'Client',

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {Template, Compiler} from 'webpack';
const pluginName = 'chunk-asset-plugin';
class ChunkAssetPlugin {
apply(compiler: Compiler) {
compiler.hooks.thisCompilation.tap(pluginName, ({mainTemplate}) => {
/* We modify webpack runtime to add an extra function called "__webpack_require__.gca"
that will allow us to get the corresponding chunk asset for a webpack chunk.
Pass it the chunkName or chunkId you want to load.
For example: if you have a chunk named "my-chunk-name" that will map to "/0a84b5e7.c8e35c7a.js" as its corresponding output path
__webpack_require__.gca("my-chunk-name") will return "/0a84b5e7.c8e35c7a.js"*/
mainTemplate.hooks.requireExtensions.tap(pluginName, (source, chunk) => {
const chunkIdToName = chunk.getChunkMaps(false).name;
const chunkNameToId = Object.create(null);
for (const chunkId of Object.keys(chunkIdToName)) {
const chunkName = chunkIdToName[chunkId];
chunkNameToId[chunkName] = chunkId;
}
const buf = [source];
buf.push('');
buf.push('// function to get chunk assets');
buf.push(
mainTemplate.requireFn +
// If chunkName is passed, we convert it to chunk id
// Note that jsonpScriptSrc is an internal webpack function
`.gca = function(chunkId) { chunkId = ${JSON.stringify(
chunkNameToId,
)}[chunkId]||chunkId; return jsonpScriptSrc(chunkId); };`,
);
return Template.asString(buf);
});
});
}
}
export default ChunkAssetPlugin;

View File

@ -1,89 +0,0 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const HtmlWebpackPlugin = require('html-webpack-plugin');
const fs = require('fs-extra');
const path = require('path');
const pluginName = 'ChunkManifestPlugin';
class ChunkManifestPlugin {
constructor(options) {
this.options = {
filename: 'manifest.json',
outputPath: null,
manifestVariable: 'webpackManifest',
inlineManifest: false,
...options,
};
}
apply(compiler) {
let chunkManifest;
const {path: defaultOutputPath, publicPath} = compiler.options.output;
// Build the chunk mapping
compiler.hooks.afterCompile.tapAsync(pluginName, (compilation, done) => {
const assets = {};
const assetsMap = {};
// eslint-disable-next-line
for (const chunkGroup of compilation.chunkGroups) {
if (chunkGroup.name) {
const files = [];
// eslint-disable-next-line
for (const chunk of chunkGroup.chunks) {
files.push(...chunk.files);
}
assets[chunkGroup.name] = files.filter(f => f.slice(-4) !== '.map');
assetsMap[chunkGroup.name] = files
.filter(
f =>
f.slice(-4) !== '.map' &&
f.slice(0, chunkGroup.name.length) === chunkGroup.name,
)
.map(filename => `${publicPath}${filename}`);
}
}
chunkManifest = assetsMap;
if (!this.options.inlineManifest) {
const outputPath = this.options.outputPath || defaultOutputPath;
const finalPath = path.resolve(outputPath, this.options.filename);
fs.ensureDir(path.dirname(finalPath), () => {
fs.writeFile(finalPath, JSON.stringify(chunkManifest, null, 2), done);
});
} else {
done();
}
});
compiler.hooks.compilation.tap(pluginName, compilation => {
// inline to html-webpack-plugin <head> tag
if (this.options.inlineManifest) {
const hooks = HtmlWebpackPlugin.getHooks(compilation);
const {manifestVariable} = this.options;
hooks.alterAssetTagGroups.tap(pluginName, assets => {
if (chunkManifest) {
const newTag = {
tagName: 'script',
closeTag: true,
attributes: {
type: 'text/javascript',
},
innerHTML: `/*<![CDATA[*/window.${manifestVariable}=${JSON.stringify(
chunkManifest,
)};/*]]>*/`,
};
assets.headTags.unshift(newTag);
}
});
}
});
}
}
module.exports = ChunkManifestPlugin;