mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 19:21:50 +00:00
Performance: Split large dependencies into their own individual chunks (take 2) (#26594)
This commit is contained in:
parent
04d5cbccc8
commit
bd4b3c02e0
42
client/web/bundlesize.config.js
Normal file
42
client/web/bundlesize.config.js
Normal file
@ -0,0 +1,42 @@
|
||||
const config = {
|
||||
files: [
|
||||
/**
|
||||
* Our main entry JavaScript bundles, contains core logic that is loaded on every page.
|
||||
*/
|
||||
{
|
||||
path: '../../ui/assets/scripts/*.bundle.js.br',
|
||||
maxSize: '500kb',
|
||||
compression: 'none',
|
||||
},
|
||||
/**
|
||||
* Our generated application chunks. Matches the deterministic id generated by Webpack.
|
||||
*
|
||||
* Note: The vast majority of our chunks are under 200kb, this threshold is bloated as we treat the Monaco editor as a normal chunk.
|
||||
* We should consider not doing this, as it is much larger than other chunks and we would likely benefit from caching this differently.
|
||||
* Issue to improve this: https://github.com/sourcegraph/sourcegraph/issues/26573
|
||||
*/
|
||||
{
|
||||
path: '../../ui/assets/scripts/[0-9]*.chunk.js.br',
|
||||
maxSize: '500kb',
|
||||
compression: 'none',
|
||||
},
|
||||
/**
|
||||
* Our generated worker files.
|
||||
*/
|
||||
{
|
||||
path: '../../ui/assets/*.worker.js.br',
|
||||
maxSize: '250kb',
|
||||
compression: 'none',
|
||||
},
|
||||
/**
|
||||
* Our main entry CSS bundle, contains core styles that are loaded on every page.
|
||||
*/
|
||||
{
|
||||
path: '../../ui/assets/styles/app.*.css.br',
|
||||
maxSize: '50kb',
|
||||
compression: 'none',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
@ -4,22 +4,17 @@ import path from 'path'
|
||||
import * as esbuild from 'esbuild'
|
||||
|
||||
import { STATIC_ASSETS_PATH } from '../utils'
|
||||
import { WebpackManifest } from '../webpack/get-html-webpack-plugins'
|
||||
|
||||
export const assetPathPrefix = '/.assets'
|
||||
|
||||
interface Manifest {
|
||||
'app.js': string
|
||||
'app.css': string
|
||||
isModule: boolean
|
||||
}
|
||||
|
||||
export const getManifest = (): Manifest => ({
|
||||
'app.js': path.join(assetPathPrefix, 'scripts/app.js'),
|
||||
'app.css': path.join(assetPathPrefix, 'scripts/app.css'),
|
||||
export const getManifest = (): WebpackManifest => ({
|
||||
appBundle: path.join(assetPathPrefix, 'scripts/app.js'),
|
||||
cssBundle: path.join(assetPathPrefix, 'scripts/app.css'),
|
||||
isModule: true,
|
||||
})
|
||||
|
||||
const writeManifest = async (manifest: Manifest): Promise<void> => {
|
||||
const writeManifest = async (manifest: WebpackManifest): Promise<void> => {
|
||||
const manifestPath = path.join(STATIC_ASSETS_PATH, 'webpack.manifest.json')
|
||||
await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2))
|
||||
}
|
||||
|
||||
@ -136,10 +136,7 @@ async function startEsbuildDevelopmentServer({
|
||||
csrfTokenCookieMiddleware,
|
||||
}: DevelopmentServerInit): Promise<void> {
|
||||
const manifest = getManifest()
|
||||
const htmlPage = getHTMLPage({
|
||||
head: `<link rel="stylesheet" href="${manifest['app.css']}">`,
|
||||
bodyEnd: `<script src="${manifest['app.js']}" type="module"></script>`,
|
||||
})
|
||||
const htmlPage = getHTMLPage(manifest)
|
||||
|
||||
await esbuildDevelopmentServer({ host: '0.0.0.0', port: SOURCEGRAPH_HTTPS_PORT }, app => {
|
||||
app.use(csrfTokenCookieMiddleware)
|
||||
|
||||
@ -8,16 +8,33 @@ import { createJsContext, environmentConfig, STATIC_ASSETS_PATH } from '../utils
|
||||
|
||||
const { SOURCEGRAPH_HTTPS_PORT, NODE_ENV } = environmentConfig
|
||||
|
||||
interface HTMLPageData {
|
||||
head: string
|
||||
bodyEnd: string
|
||||
export interface WebpackManifest {
|
||||
/** Main app entry JS bundle */
|
||||
appBundle: string
|
||||
/** Main app entry CSS bundle, only used in production mode */
|
||||
cssBundle?: string
|
||||
/** Runtime bundle, only used in development mode */
|
||||
runtimeBundle?: string
|
||||
/** React entry bundle, only used in production mode */
|
||||
reactBundle?: string
|
||||
/** If script files should be treated as JS modules. Required for esbuild bundle. */
|
||||
isModule?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an HTML page similar to `cmd/frontend/internal/app/ui/app.html` but when running
|
||||
* without the `frontend` service.
|
||||
*
|
||||
* Note: This page should be kept as close as possible to `app.html` to avoid any inconsistencies
|
||||
* between our development server and the actual production server.
|
||||
*/
|
||||
export const getHTMLPage = ({ head, bodyEnd }: HTMLPageData): string => `
|
||||
export const getHTMLPage = ({
|
||||
appBundle,
|
||||
cssBundle,
|
||||
runtimeBundle,
|
||||
reactBundle,
|
||||
isModule,
|
||||
}: WebpackManifest): string => `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@ -26,7 +43,7 @@ export const getHTMLPage = ({ head, bodyEnd }: HTMLPageData): string => `
|
||||
<meta name="viewport" content="width=device-width, viewport-fit=cover" />
|
||||
<meta name="referrer" content="origin-when-cross-origin"/>
|
||||
<meta name="color-scheme" content="light dark"/>
|
||||
${head}
|
||||
${cssBundle ? `<link rel="stylesheet" href="${cssBundle}">` : ''}
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
@ -39,19 +56,40 @@ export const getHTMLPage = ({ head, bodyEnd }: HTMLPageData): string => `
|
||||
createJsContext({ sourcegraphBaseUrl: `http://localhost:${SOURCEGRAPH_HTTPS_PORT}` })
|
||||
)}
|
||||
</script>
|
||||
${bodyEnd}
|
||||
|
||||
${runtimeBundle ? `<script src="${runtimeBundle}"></script>` : ''}
|
||||
${reactBundle ? `<script src="${reactBundle}" ${isModule ? 'type="module"' : ''}></script>` : ''}
|
||||
<script src="${appBundle}" ${isModule ? 'type="module"' : ''}></script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
/**
|
||||
* Search a list of file strings for a specific file.
|
||||
* Only uses the file prefix to allow matching against content-hashed filenames.
|
||||
*/
|
||||
const getBundleFromPath = (files: string[], filePrefix: string): string | undefined =>
|
||||
files.find(file => file.startsWith(`/.assets/${filePrefix}`))
|
||||
|
||||
export const getHTMLWebpackPlugins = (): WebpackPluginInstance[] => {
|
||||
const htmlWebpackPlugin = new HtmlWebpackPlugin({
|
||||
// `TemplateParameter` can be mutated. We need to tell TS that we didn't touch it.
|
||||
templateContent: (({ htmlWebpackPlugin }: TemplateParameter): string =>
|
||||
getHTMLPage({
|
||||
head: htmlWebpackPlugin.tags.headTags.filter(tag => tag.tagName !== 'script').toString(),
|
||||
bodyEnd: htmlWebpackPlugin.tags.headTags.filter(tag => tag.tagName === 'script').toString(),
|
||||
})) as Options['templateContent'],
|
||||
templateContent: (({ htmlWebpackPlugin }: TemplateParameter): string => {
|
||||
const { files } = htmlWebpackPlugin
|
||||
|
||||
const appBundle = getBundleFromPath(files.js, 'scripts/app')
|
||||
|
||||
if (!appBundle) {
|
||||
throw new Error('Could not find any entry bundle')
|
||||
}
|
||||
|
||||
return getHTMLPage({
|
||||
appBundle,
|
||||
cssBundle: getBundleFromPath(files.css, 'styles/app'),
|
||||
runtimeBundle: getBundleFromPath(files.js, 'scripts/runtime'),
|
||||
reactBundle: getBundleFromPath(files.js, 'scripts/react'),
|
||||
})
|
||||
}) as Options['templateContent'],
|
||||
filename: path.resolve(STATIC_ASSETS_PATH, 'index.html'),
|
||||
alwaysWriteToDisk: true,
|
||||
inject: false,
|
||||
|
||||
@ -30,17 +30,10 @@
|
||||
"eslint": "eslint --cache '**/*.[tj]s?(x)'",
|
||||
"stylelint": "stylelint 'src/**/*.scss' --quiet",
|
||||
"browserslist": "browserslist",
|
||||
"analyze-bundle": "NODE_ENV=production ENTERPRISE=1 WEBPACK_ANALYZER=1 yarn build",
|
||||
"bundlesize": "bundlesize"
|
||||
"analyze-bundle": "WEBPACK_USE_NAMED_CHUNKS=true NODE_ENV=production ENTERPRISE=1 WEBPACK_ANALYZER=1 yarn build",
|
||||
"bundlesize": "bundlesize --config=./bundlesize.config.js"
|
||||
},
|
||||
"jest": {
|
||||
"testURL": "http://localhost:3080"
|
||||
},
|
||||
"bundlesize": [
|
||||
{
|
||||
"path": "../../ui/assets/scripts/*.js",
|
||||
"maxSize": "600kb",
|
||||
"compression": "brotli"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,6 +108,15 @@ const config = {
|
||||
}),
|
||||
new CssMinimizerWebpackPlugin(),
|
||||
],
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
react: {
|
||||
test: /[/\\]node_modules[/\\](react|react-dom)[/\\]/,
|
||||
name: 'react',
|
||||
chunks: 'all',
|
||||
},
|
||||
},
|
||||
},
|
||||
...(isDevelopment && {
|
||||
// Running multiple entries on a single page that do not share a runtime chunk from the same compilation is not supported.
|
||||
// https://github.com/webpack/webpack-dev-server/issues/2792#issuecomment-808328432
|
||||
@ -125,10 +134,11 @@ const config = {
|
||||
output: {
|
||||
path: path.join(rootPath, 'ui', 'assets'),
|
||||
// Do not [hash] for development -- see https://github.com/webpack/webpack-dev-server/issues/377#issuecomment-241258405
|
||||
// Note: [name] will vary depending on the Webpack chunk. If specified, it will use a provided chunk name, otherwise it will fallback to a deterministic id.
|
||||
filename:
|
||||
mode === 'production' && !useNamedChunks ? 'scripts/[name].[contenthash].bundle.js' : 'scripts/[name].bundle.js',
|
||||
chunkFilename:
|
||||
mode === 'production' && !useNamedChunks ? 'scripts/[id]-[contenthash].chunk.js' : 'scripts/[id].chunk.js',
|
||||
mode === 'production' && !useNamedChunks ? 'scripts/[name]-[contenthash].chunk.js' : 'scripts/[name].chunk.js',
|
||||
publicPath: '/.assets/',
|
||||
globalObject: 'self',
|
||||
pathinfo: false,
|
||||
|
||||
@ -64,6 +64,7 @@
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
{{if .Manifest.AppJSRuntimeBundlePath}}<script src="{{.Manifest.AppJSRuntimeBundlePath}}"></script>{{end}}
|
||||
<script src="{{.Manifest.ReactJSBundlePath}}" {{if .Manifest.IsModule}}type="module"{{end}}></script>
|
||||
<script src="{{.Manifest.AppJSBundlePath}}" {{if .Manifest.IsModule}}type="module"{{end}}></script>
|
||||
{{.Injected.BodyBottom}}
|
||||
</body>
|
||||
|
||||
@ -226,7 +226,7 @@
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
"babel-preset-react-app": "^10.0.0",
|
||||
"browserslist": "^4.17.4",
|
||||
"bundlesize2": "^0.0.30",
|
||||
"bundlesize2": "^0.0.31",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chalk": "^4.1.0",
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
package assets
|
||||
|
||||
type WebpackManifest struct {
|
||||
// ReactJSBundlePath contains the file name of the ReactJS
|
||||
// dependency bundle, that is required by our main app bundle.
|
||||
ReactJSBundlePath string `json:"react.js"`
|
||||
// AppJSBundlePath contains the file name of the main
|
||||
// Webpack bundle that serves as the entrypoint
|
||||
// for the webapp code.
|
||||
|
||||
@ -7946,10 +7946,10 @@ buffer@^5.1.0, buffer@^5.2.1, buffer@^5.5.0, buffer@^5.7.0:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.1.13"
|
||||
|
||||
bundlesize2@^0.0.30:
|
||||
version "0.0.30"
|
||||
resolved "https://registry.npmjs.org/bundlesize2/-/bundlesize2-0.0.30.tgz#a90de469eaaca9840bd20562a011f258f5c97cd9"
|
||||
integrity sha512-dVNC4zGdaRMMf+KK4Xf+wsPIuuDYrgDtZaX5gb1jX2vRCSOgrh2ka0Y3BGmcwMC0vViBg38yoap9AUXLBItZFA==
|
||||
bundlesize2@^0.0.31:
|
||||
version "0.0.31"
|
||||
resolved "https://registry.npmjs.org/bundlesize2/-/bundlesize2-0.0.31.tgz#cef6830dbbe5360e27fce07593ec0f4e3e5355d9"
|
||||
integrity sha512-MdzJW/u+n+0jH0Uz78g8WENHAW7QNUdLD/c8aLuPB/aCIwt52zMJ4fc2fBU2y1K2iMwE/9+JoR8ojsAF0r0Xjw==
|
||||
dependencies:
|
||||
bytes "^3.1.0"
|
||||
chalk "^4.0.0"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user