[optimizer] More aggressive chunking of common/vendor code (#15907)

Previously, we were not aggressive in combining common code which resulted in duplicates included in the bundles.

As an example `node_modules/elasticsearch-browser/elasticsearch.angular.js` is present in the following chunks:

* kibana.bundle.js
* dashboardViewer.bundle.js
* apm.bundle.js
* monitoring.bundle.js
* ml.bundle.js
* timelion.bundle.js
* graph.bundle.js

Vendored code (anything inside node_modules) is placed in vendors.bundle.js while everything else with more than two references is placed in commons.bundle.js.

This has a couple positive side-effects (numbers are with x-pack & canvas):

* Decreased build time. Seeing builds go from 475.76 seconds to 274.72.
* Decreased memory overhead. Uses roughly 1/3 the memory overhead.
* Decreased bundle size. A 68% reduction in overall bundle size. Going from 66.16 MB to 21.13 MB.

Signed-off-by: Tyler Smalley <tyler.smalley@elastic.co>
This commit is contained in:
Tyler Smalley 2018-01-15 08:15:28 -08:00 committed by GitHub
parent cae1a82140
commit 41d905309b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 58 additions and 9 deletions

View file

@ -3,10 +3,6 @@ import { writeFile } from 'fs';
import Boom from 'boom';
import ExtractTextPlugin from 'extract-text-webpack-plugin';
import webpack from 'webpack';
import CommonsChunkPlugin from 'webpack/lib/optimize/CommonsChunkPlugin';
import DefinePlugin from 'webpack/lib/DefinePlugin';
import UglifyJsPlugin from 'webpack/lib/optimize/UglifyJsPlugin';
import NoEmitOnErrorsPlugin from 'webpack/lib/NoEmitOnErrorsPlugin';
import Stats from 'webpack/lib/Stats';
import webpackMerge from 'webpack-merge';
@ -98,6 +94,8 @@ export default class BaseOptimizer {
});
}
const nodeModulesPath = fromRoot('node_modules');
/**
* Adds a cache loader if we're running in dev mode. The reason we're not adding
* the cache-loader when running in production mode is that it creates cache
@ -141,12 +139,20 @@ export default class BaseOptimizer {
allChunks: true
}),
new CommonsChunkPlugin({
new webpack.optimize.CommonsChunkPlugin({
name: 'commons',
filename: 'commons.bundle.js'
filename: 'commons.bundle.js',
minChunks: 2,
}),
new NoEmitOnErrorsPlugin(),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendors',
filename: 'vendors.bundle.js',
// only combine node_modules from Kibana
minChunks: module => module.context && module.context.indexOf(nodeModulesPath) !== -1
}),
new webpack.NoEmitOnErrorsPlugin(),
],
module: {
@ -233,12 +239,12 @@ export default class BaseOptimizer {
return webpackMerge(commonConfig, {
plugins: [
new DefinePlugin({
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': '"production"'
}
}),
new UglifyJsPlugin({
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},

View file

@ -25,6 +25,10 @@ export default async (kbnServer, server, config) => {
await uiBundles.writeEntryFiles();
// Not all entry files produce a css asset. Ensuring they exist prevents
// an error from occuring when the file is missing.
await uiBundles.ensureStyleFiles();
// in prod, only bundle when someing is missing or invalid
const reuseCache = config.get('optimize.useBundleCache')
? await uiBundles.areAllBundleCachesValid()

View file

@ -27,6 +27,8 @@ export default class WatchOptimizer extends BaseOptimizer {
this.status$.subscribe(this.onStatusChangeHandler);
await this.uiBundles.writeEntryFiles();
await this.uiBundles.ensureStyleFiles();
await this.initCompiler();
this.compiler.plugin('watch-run', this.compilerRunStartHandler);

View file

@ -33,6 +33,10 @@ export class UiBundle {
return this._controller.resolvePath(`${this.getId()}.entry.js`);
}
getStylePath() {
return this._controller.resolvePath(`${this.getId()}.style.css`);
}
getOutputPath() {
return this._controller.resolvePath(`${this.getId()}.bundle.js`);
}
@ -63,6 +67,20 @@ export class UiBundle {
));
}
async hasStyleFile() {
return await fcb(cb => {
return stat(this.getStylePath(), error => {
cb(null, !(error && error.code === 'ENOENT'));
});
});
}
async touchStyleFile() {
return await fcb(cb => (
writeFile(this.getStylePath(), '', 'utf8', cb)
));
}
async clearBundleFile() {
try {
await fcb(cb => unlink(this.getOutputPath(), cb));

View file

@ -146,6 +146,16 @@ export class UiBundlesController {
}
}
async ensureStyleFiles() {
await this.ensureDir();
for (const bundle of this._bundles) {
if (!await bundle.hasStyleFile()) {
await bundle.touchStyleFile();
}
}
}
hashBundleEntries() {
const hash = createHash('sha1');
for (const bundle of this._bundles) {

View file

@ -120,8 +120,11 @@ block content
return anchor.href;
}
var files = [
bundleFile('vendors.style.css'),
bundleFile('commons.style.css'),
bundleFile('#{app.getId()}.style.css'),
bundleFile('vendors.bundle.js'),
bundleFile('commons.bundle.js'),
bundleFile('#{app.getId()}.bundle.js')
];

View file

@ -37,8 +37,11 @@ module.exports = function (grunt) {
// list of files / patterns to load in the browser
files: [
'http://localhost:5610/bundles/vendors.bundle.js',
'http://localhost:5610/bundles/commons.bundle.js',
'http://localhost:5610/bundles/tests.bundle.js',
'http://localhost:5610/bundles/vendors.style.css',
'http://localhost:5610/bundles/commons.style.css',
'http://localhost:5610/bundles/tests.style.css'
],
@ -126,8 +129,11 @@ module.exports = function (grunt) {
singleRun: true,
options: {
files: [
'http://localhost:5610/bundles/vendors.bundle.js',
'http://localhost:5610/bundles/commons.bundle.js',
`http://localhost:5610/bundles/tests.bundle.js?shards=${TOTAL_CI_SHARDS}&shard_num=${n}`,
'http://localhost:5610/bundles/vendors.style.css',
'http://localhost:5610/bundles/commons.style.css',
'http://localhost:5610/bundles/tests.style.css'
]