├── src ├── file1.bundle.js └── file2.bundle.js ├── .gitignore ├── log-module-build-plugin.js ├── package.json ├── webpack.config.js ├── README.md └── server.js /src/file1.bundle.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/file2.bundle.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /log-module-build-plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a simple plugin to log the name of a module whenever it is built 3 | * by webpack. Without changes to a file, we expect to only see this once per 4 | * file. 5 | */ 6 | module.exports = class LogModuleBuildPlugin { 7 | apply(compiler) { 8 | compiler.plugin('compilation', (compilation) => { 9 | compilation.plugin('build-module', (module) => { 10 | console.log(`building: ${module.identifier()}`); 11 | }); 12 | }); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-build-as-needed", 3 | "version": "1.0.0", 4 | "description": "A demo repository for building only as needed with WebpackDevServer.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "Gary Borton ", 11 | "license": "MIT", 12 | "dependencies": { 13 | "express": "^5.0.0-alpha.4", 14 | "glob": "^7.1.2", 15 | "webpack": "^3.8.1", 16 | "webpack-dev-server": "^2.9.4" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const glob = require('glob'); 3 | 4 | const LogModuleBuildPlugin = require('./log-module-build-plugin'); 5 | 6 | const entryFiles = glob.sync('./src/*'); // use w/e pattern makes sense in your code base. 7 | 8 | const config = { 9 | name: 'myFancyCompiler', 10 | entry:{}, 11 | output: { 12 | path: path.resolve('dist'), 13 | publicPath: '/dist/', 14 | filename: '[name].js', 15 | }, 16 | plugins: [ 17 | new LogModuleBuildPlugin() 18 | ] 19 | }; 20 | 21 | /** 22 | * Takes an array of files [src/file1.bundle.js] 23 | * and adds an entry config for each one... 24 | * { 25 | * 'file1.bundle': ['absolute/path/to/src/file1.bundle.js'] 26 | * } 27 | * Note: We're using an array for all entry points. 28 | */ 29 | entryFiles.forEach(file => { 30 | const key = path.basename(file).replace(path.extname(file), ''); 31 | const value = path.resolve(file); 32 | config.entry[key] = [value]; 33 | }); 34 | 35 | module.exports = config; 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Building only as needed with WebpackDevServer 2 | 3 | Sometimes, for super huge builds, webpack is slow. One method I've used for speeding up the dev experience, is force a webpack-dev-server to only build the entry points that developers request instead of the entire build. 4 | 5 | This can dramatically increase dev boot up times, as you're potentially limiting the amount of work to a small percentage of the total build. 6 | 7 | ## Understanding this demo 8 | 9 | This demo application is super simple. There are two bundles in our webpack config (added via a glob, this expands infinitely and programmatically), `file1.bundle.js` and `files2.bundle.js`. Neither contain logic, and that's ok. 10 | 11 | When you boot the server, we remove all but one entry from the config, and store a reference to everything that is removed. As you request bundles that have been removed, we add them to our compiler, and invalidate. 12 | 13 | This triggers a rebuild with your new bundle, and delays serving responses until the compiler is ready. 14 | 15 | You can see this in action by starting the server then loading these files in order. 16 | 17 | 1. http://localhost:3000/dist/file1.bundle.js 18 | 2. http://localhost:3000/dist/file2.bundle.js 19 | 20 | This should trigger the following output. 21 | ``` 22 | building: multi /src/file1.bundle.js 23 | Listening on port: 3000 24 | building: /src/file1.bundle.js 25 | 26 | webpack: Compiled successfully. 27 | Found a request for an unbuilt bundle "file2.bundle", adding to compiler: "myFancyCompiler" 28 | webpack: Compiling... 29 | building: /src/file1.bundle.js 30 | building: multi /src/file2.bundle.js 31 | webpack: wait until bundle finished: /dist/file2.bundle.js 32 | building: /src/file2.bundle.js 33 | ``` 34 | 35 | The vast majority of the work being done is in `./server.js`'s `createBuildOnRequestMiddleware`. 36 | 37 | Note: It's difficult to read from the pasted logs, but `file2.bundle.js` isn't build until you request it. 38 | 39 | ## Gotcha's 40 | 41 | 1. You need to be careful when using the CommonsChunk plugin. 42 | - This plugin needs a lot of context. If you're using it, you should be compiling any affected chunks together. Otherwise you could hit unexpected module scope issues in production. They're rare, but confusing to debug since it would behave different in dev. 43 | 2. Webpack currently doesn't timestamp without changes. 44 | - Webpack uses file timestamps to determine whether a module has changed and needs to be rebuilt. It's currently only setting these when it detects a change with its watcher. This means that until you save a file, any new entry points added will trigger a full rebuild. 45 | - This repo exists partially as a resource to correct this "bug". 46 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const express = require('express'); 4 | const WebpackDevServer = require('webpack-dev-server'); 5 | 6 | const MultiEntryPlugin = require('webpack/lib/MultiEntryPlugin'); 7 | 8 | const config = require('./webpack.config'); 9 | const publicPath = '/dist/'; 10 | 11 | /** 12 | * Takes a request "/dist/file1.bundle.js" 13 | * returns a key from config.entry "file1.bundle" 14 | */ 15 | function convertReqPathToBundleName(reqPath) { 16 | return path.basename(reqPath).replace(path.extname(reqPath), ''); 17 | } 18 | 19 | function createBuildOnRequestMiddleware(config) { 20 | // hold onto a reference to our original config.entry, 21 | // and clear out the entry on the config. We'll dynamically 22 | // re-add as needed. 23 | const allEntries = config.entry; 24 | config.entry = {}; 25 | 26 | // Webpack validation will fail if you create a compiler with no entry 27 | // points, so we add the first one back in. 28 | const firstEntry = Object.keys(allEntries)[0]; 29 | config.entry[firstEntry] = allEntries[firstEntry]; 30 | 31 | const compiler = webpack(config); 32 | const webpackDevServer = new WebpackDevServer(compiler, { 33 | publicPath, 34 | stats: 'errors-only', 35 | }); 36 | 37 | const devServerWrapper = express(); 38 | 39 | /** 40 | * Before we hook up the webpackDevServer, we need to be sure we're 41 | * compiling everything needed. 42 | * This makes sure our compiler is in the correct state before letting 43 | * webpackDevServer handle all the requests. 44 | */ 45 | devServerWrapper.use(function(req, res, next) { 46 | const entryKey = convertReqPathToBundleName(req.path); 47 | if(!config.entry[entryKey] && allEntries[entryKey]) { 48 | console.log(`Found a request for an unbuilt bundle "${entryKey}", adding to compiler: "${compiler.name}"`); 49 | config.entry[entryKey] = allEntries[entryKey]; 50 | 51 | // This only works for entries that are composed of arrays. 52 | // { 'file1.bundle': ['...'] } vs { 'file1.bundle': '...' } 53 | compiler.apply(new MultiEntryPlugin(null, allEntries[entryKey], entryKey)); 54 | 55 | // This is sort of hacky, but it was the easiest way to invalidate 56 | // webpack + watcher. 57 | webpackDevServer.middleware.invalidate(); 58 | } 59 | next(); 60 | }); 61 | 62 | devServerWrapper.use(webpackDevServer.app); 63 | 64 | return devServerWrapper; 65 | } 66 | 67 | const port = 3000; 68 | const app = express(); 69 | 70 | /** 71 | * Load the webpackDevServer as middleware in our express server. 72 | */ 73 | app.use(createBuildOnRequestMiddleware(config)); 74 | 75 | app.listen(port, () => { 76 | console.log(`Listening on port: ${port}`); 77 | }); 78 | --------------------------------------------------------------------------------