mirror of
https://github.com/facebook/docusaurus.git
synced 2025-12-26 01:33:02 +00:00
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:
parent
4f0ae6d117
commit
daa71b2eff
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ module.exports = `
|
|||
<meta name="viewport" content="width=device-width">
|
||||
<meta name="generator" content="Docusaurus">
|
||||
<%- headTags %>
|
||||
<%- chunkManifestScript %>
|
||||
<% metaAttributes.forEach((metaAttribute) => { %>
|
||||
<%- metaAttribute %>
|
||||
<% }); %>
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
Loading…
Reference in New Issue