Skip to content

Commit 8bc70e7

Browse files
Merge pull request #59 from faceyspacey/agressive-splitting-wp4
feat(src/flushChunks.js): Support Webpack 4 Aggressive Code Splitting / Chunking
2 parents 0db55a6 + 2ce881c commit 8bc70e7

File tree

5 files changed

+167
-62
lines changed

5 files changed

+167
-62
lines changed

README.md

Lines changed: 78 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
# Webpack Flush Chunks
1111

1212

13-
1413
<p align="center">
1514
<a href="https://www.npmjs.com/package/webpack-flush-chunks">
1615
<img src="https://img.shields.io/npm/v/webpack-flush-chunks.svg" alt="Version" />
@@ -39,8 +38,12 @@
3938

4039
<p align="center">
4140
🍾🍾🍾 <a href="https://github.com/faceyspacey/universal-demo">GIT CLONE LOCAL DEMO</a> 🚀🚀🚀
41+
<br> Webpack 4 demo update in progress
4242
</p>
4343

44+
> **Now supports Webpack 4 aggressive code splitting**
45+
We have updated `webpack-flush-chunks` to now support more complex code splitting! `webpack-flush-chunks` enables developers to leverage smarter and less wasteful chunking methods avaliable to developers inside of Webpack.
46+
4447
<p align="center">
4548
<img src="./poo.jpg" height="350" />
4649
</p>
@@ -52,7 +55,7 @@ import { flushChunkNames } from 'react-universal-component/server'
5255
import flushChunks from 'webpack-flush-chunks'
5356

5457
const app = ReactDOMServer.renderToString(<App />)
55-
const { js, styles, cssHash } = flushChunks(webpackStats, {
58+
const { js, styles } = flushChunks(webpackStats, {
5659
chunkNames: flushChunkNames()
5760
})
5861

@@ -64,13 +67,65 @@ res.send(`
6467
</head>
6568
<body>
6669
<div id="root">${app}</div>
67-
${cssHash}
6870
${js}
6971
</body>
7072
</html>
7173
`)
7274
```
7375

76+
## Webpack 4 Aggressive Code Splitting Support
77+
78+
This plugin allows for complex code splitting to be leveraged for improved caching and less code duplication!
79+
Below are two examples of well tested splitting configurations. If you experience any issues with bespoke optimization configurations, we would love to hear about it!
80+
81+
#### Before:
82+
Before this update, developers were limited to a single chunk stratagey. What the stratagey did was give developers a similar chunk method to `CommonsChunkPlugin` that was used in Webpack 3. We did not support `AggressiveSplittingPlugin`
83+
84+
```js
85+
optimization: {
86+
runtimeChunk: {
87+
name: 'bootstrap'
88+
},
89+
splitChunks: {
90+
chunks: 'initial',
91+
cacheGroups: {
92+
vendors: {
93+
test: /[\\/]node_modules[\\/]/,
94+
name: 'vendor'
95+
}
96+
}
97+
}
98+
},
99+
```
100+
#### After:
101+
Now you can use many flexible code splitting methods, like the one below. Webpack encourages aggressive code splitting methods, so we jumped on the bandwagon and did the upgrades. Just like before, we use the chunkNames generated - then we can look within the Webpack 4 chunk graph and resolve any other dependencies or automatically generated chunks that consist as part of the initial chunk.
102+
103+
We can load the nested chunks in the correct order as required and if many chunks share a common chunk, we ensure they load in the correct order, so that vendor chunks are always available to all chunks depending on them without creating any duplicate requests or chunk calls.
104+
105+
```js
106+
optimization: {
107+
splitChunks: {
108+
chunks: 'async',
109+
minSize: 30000,
110+
minChunks: 1,
111+
maxAsyncRequests: 5,
112+
maxInitialRequests: 3,
113+
automaticNameDelimiter: '~',
114+
name: true,
115+
cacheGroups: {
116+
vendors: {
117+
test: /[\\/]node_modules[\\/]/,
118+
priority: -10
119+
},
120+
default: {
121+
minChunks: 2,
122+
priority: -20,
123+
reuseExistingChunk: true
124+
}
125+
}
126+
}
127+
}
128+
```
74129
The code has been cracked for while now for Server Side Rendering and Code-Splitting *individually*. Accomplishing both *simultaneously* has been an impossibility without jumping through major hoops or using a *framework*, specifically Next.js. Our tools are for "power users" that prefer the *frameworkless* approach.
75130

76131
*Webpack Flush Chunks* is essentially the backend to universal rendering components like [React Universal Component](https://github.com/faceyspacey/react-universal-component). It works with any "universal" component/module that buffers a list of `moduleIds` or `chunkNames` evaluated.
@@ -101,11 +156,12 @@ yarn add --dev babel-plugin-universal-import extract-css-chunks-webpack-plugin
101156
```
102157
- ***[Babel Plugin Universal Import](https://github.com/faceyspacey/babel-plugin-universal-import)*** is used to make `react-universal-component` as frictionless as possible. It removes the need to provide additional options to insure synchronous rendering happens on the server and on the client on initial load. These packages aren't required, but usage as frictionless as possible.
103158

104-
- ***[Extract Css Chunks Webpack Plugin](https://github.com/faceyspacey/extract-css-chunks-webpack-plugin)*** is another companion package made to complete the CSS side of the code-splitting dream. It uses the `cssHash` string to asynchronously request CSS assets as part of a "dual import" when calling `import()`.
159+
- ***[Extract Css Chunks Webpack Plugin](https://github.com/faceyspacey/extract-css-chunks-webpack-plugin)*** is another companion package made to complete the CSS side of the code-splitting dream. Its also a standalone plugin thats great for codesplitting css, with **built-in HMR**
160+
105161

106162

163+
~~*If you like to move fast, git clone the [universal-demo](https://github.com/faceyspacey/universal-demo)*.~~ We are working on a Webpack 4 demo
107164

108-
*If you like to move fast, git clone the [universal-demo](https://github.com/faceyspacey/universal-demo)*.
109165

110166
## How It Works
111167

@@ -142,14 +198,6 @@ Before we examine how to use `flushChunks/flushFiles`, let's take a look at the
142198

143199
<!-- after entry chunks -->
144200
<script type='text/javascript' src='/static/main.js'></script>
145-
146-
<!-- stylsheets that will be requested when import() is called -->
147-
<script>
148-
window.__CSS_CHUNKS__ = {
149-
Foo: '/static/Foo.css',
150-
Bar: '/static/Bar.css'
151-
}
152-
</script>
153201
</body>
154202
```
155203

@@ -191,7 +239,7 @@ import flushChunks from 'webpack-flush-chunks'
191239
192240
const app = ReactDOMServer.renderToString(<App />)
193241
const chunkNames = flushChunkNames()
194-
const { js, styles, cssHash } = flushChunks(stats, { chunkNames })
242+
const { js, styles } = flushChunks(stats, { chunkNames })
195243
196244
res.send(`
197245
<!doctype html>
@@ -201,7 +249,6 @@ res.send(`
201249
</head>
202250
<body>
203251
<div id="root">${app}</div>
204-
${cssHash}
205252
${js}
206253
</body>
207254
</html>
@@ -223,22 +270,22 @@ flushChunks(stats, {
223270
})
224271
```
225272
226-
227273
- **chunkNames** - ***array of chunks flushed from `react-universal-component`
228274
229-
- **before** - ***array of named entries that come BEFORE your dynamic chunks:*** A typical
275+
- **before** - ***array of named entries that come BEFORE your dynamic chunks:*** ~~A typical
230276
pattern is to create a `vendor` chunk. A better strategy is to create a `vendor` and a `bootstrap` chunk. The "bootstrap"
231277
chunk is a name provided to the `CommonsChunkPlugin` which has no entry point specified for it. The plugin by default removes
232278
webpack bootstrap code from the named `vendor` common chunk and puts it in the `bootstrap` chunk. This is a common pattern because
233279
the webpack bootstrap code has info about the chunks/modules used in your bundle and is likely to change, which means to cache
234280
your `vendor` chunk you need to extract the bootstrap code into its own small chunk file. If this is new to you, don't worry.
235-
[Below](#webpack-configuration) you will find examples for exactly how to specify your Webpack config. Lastly, you do not need to
236-
provide this option if you have a `bootstrap` chunk, or `vendor` chunk or both, as those are the defaults.
281+
[Below](#webpack-configuration) you will find examples for exactly how to specify your Webpack config. Lastly, you do not need to provide this option if you have a `bootstrap` chunk, or `vendor` chunk or both, as those are the defaults.~~
282+
283+
Mostly related to Webpack 2 & 3. It is still very useful if you need to load a specific chunk name **first** `webpack-flush-chunks` now can rely on a better chunk graph provided by Webpack 4 - chunks are loaded in the correct order with more autonomy.
237284

238285
- **after** - ***array of named entries that come AFTER your dynamic chunks:***
239-
Similar to `before`, `after` contains an array of chunks you want to come after the dynamic chunks that
286+
~~Similar to `before`, `after` contains an array of chunks you want to come after the dynamic chunks that
240287
your universal component flushes. Typically you have just a `main` chunk, and if that's the case, you can ignore this option,
241-
as that's the default.
288+
as that's the default.~~
242289

243290
- **outputPath** - ***absolute path to the directory containing your client build:*** This is only needed if serving css
244291
embedded in your served response HTML, rather than links to external stylesheets. I.e. if you are using the `Css` and `css` values in the `return API` described in the next section. It's needed to determine where in the file system to find the CSS that needs to be extract into
@@ -303,32 +350,23 @@ module: {
303350
},
304351
{
305352
test: /\.css$/,
306-
use: ExtractCssChunks.extract({
307-
use: {
308-
loader: 'css-loader',
309-
options: {
310-
modules: true,
311-
localIdentName: '[name]__[local]--[hash:base64:5]'
312-
}
353+
use: [
354+
ExtractCssChunks.loader,
355+
{
356+
loader: 'css-loader',
357+
options: {
358+
modules: true,
359+
localIdentName: '[name]__[local]--[hash:base64:5]'
313360
}
314-
})
361+
]
315362
}
316363
]
317364
},
318365
plugins: [
319-
new ExtractCssChunks,
320-
new webpack.optimize.CommonsChunkPlugin({
321-
names: ['bootstrap'], // notice there is no "bootstrap" named entry
322-
filename: '[name].js',
323-
minChunks: Infinity
324-
})
366+
new ExtractCssChunks(),
325367
...
326368
```
327369

328-
- The `CommonsChunkPlugin` with a `"bootstrap"` entry ***which does NOT in fact exist (notice there is no `entry` for it)*** insures that a separate chunk is created just for webpack bootstrap code.
329-
This moves the webpack bootstrap code out of your `main` entry chunk so that it can also run before your dynamic
330-
chunks.
331-
332370

333371
***server:***
334372
```js
@@ -392,7 +430,7 @@ const chunkNames = flushChunkNames()
392430
const scripts = flushFiles(stats, { chunkNames, filter: 'js' })
393431
const styles = flushFiles(stats, { chunkNames, filter: 'css' })
394432
```
395-
> i.e. this will get you all files corresponding to flushed "dynamic" chunks, not `main`, `vendor`, etc.
433+
> i.e. this will get you all files corresponding to flushed "dynamic" chunks.
396434
397435
The only thing different with the API is that it has a `filter` option, and that it doesn't have `before`, `after` and `outputPath` options. The `filter` can be a file extension as a string, a regex, or a function: `filter: file => file.endsWith('js')`.
398436

__tests__/flushChunks.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ describe('unit tests', () => {
229229
bootstrap: ['bootstrap.js'],
230230
main: ['main.js', 'main.css']
231231
}
232-
const outputFiles = filesFromChunks(entryNames, assetsByChunkName)
232+
const outputFiles = filesFromChunks(entryNames, { assetsByChunkName })
233233

234234
expect(outputFiles).toEqual(['bootstrap.js', 'main.js', 'main.css'])
235235
})

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
"main": "dist/flushChunks.js",
66
"typings": "index.d.ts",
77
"author": "James Gillmore <[email protected]>",
8+
"contributors": [
9+
"Zack Jackson <[email protected]>"
10+
],
811
"license": "MIT",
912
"scripts": {
1013
"build": "babel src -d dist",

src/flushChunks.js

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ type Module = {
2222
}
2323

2424
export type Stats = {
25-
assetsByChunkName: FilesMap,
25+
assetsByChunkName: Object,
26+
namedChunkGroups: FilesMap,
2627
chunks: Array<Chunk>,
2728
modules: Array<Module>,
2829
publicPath: string
@@ -61,14 +62,16 @@ export default (stats: Stats, opts: Options): Api =>
6162

6263
const flushChunks = (stats: Stats, isWebpack: boolean, opts: Options = {}) => {
6364
const beforeEntries = opts.before || defaults.before
64-
const jsBefore = filesFromChunks(beforeEntries, stats.assetsByChunkName)
65+
const ffc = (assets, isWebpack) => filesFromChunks(assets, stats, isWebpack)
66+
67+
const jsBefore = ffc(beforeEntries)
6568

6669
const files = opts.chunkNames
67-
? filesFromChunks(opts.chunkNames, stats.assetsByChunkName, true)
70+
? ffc(opts.chunkNames, true)
6871
: flush(opts.moduleIds || [], stats, opts.rootDir, isWebpack)
6972

7073
const afterEntries = opts.after || defaults.after
71-
const jsAfter = filesFromChunks(afterEntries, stats.assetsByChunkName)
74+
const jsAfter = ffc(afterEntries)
7275

7376
return createApiWithCss(
7477
[...jsBefore, ...files, ...jsAfter],
@@ -87,7 +90,7 @@ const flushFiles = (stats: Stats, opts: Options2) =>
8790

8891
const flushFilesPure = (stats: Stats, isWebpack: boolean, opts: Options2) => {
8992
const files = opts.chunkNames
90-
? filesFromChunks(opts.chunkNames, stats.assetsByChunkName)
93+
? filesFromChunks(opts.chunkNames, stats)
9194
: flush(opts.moduleIds || [], stats, opts.rootDir, isWebpack)
9295

9396
const filter = opts.filter
@@ -144,16 +147,17 @@ const flushWebpack = (ids: Files, stats: Stats): Files => {
144147
}
145148

146149
/** CREATE FILES MAP */
147-
148-
const createFilesByPath = ({ chunks, modules }: Stats): FilesMap => {
149-
const filesByChunk = chunks.reduce((chunks, chunk) => {
150+
const filesByChunk = chunks =>
151+
chunks.reduce((chunks, chunk) => {
150152
chunks[chunk.id] = chunk.files
151153
return chunks
152154
}, {})
153155

156+
const createFilesByPath = ({ chunks, modules }: Stats): FilesMap => {
157+
const chunkedFiles = filesByChunk(chunks)
154158
return modules.reduce((filesByPath, module) => {
155159
const filePath = module.name
156-
const files = concatFilesAtKeys(filesByChunk, module.chunks)
160+
const files = concatFilesAtKeys(chunkedFiles, module.chunks)
157161

158162
filesByPath[filePath] = files.filter(isUnique)
159163
return filesByPath
@@ -172,6 +176,13 @@ const createFilesByModuleId = (stats: Stats): FilesMap => {
172176
}, {})
173177
}
174178

179+
const findChunkById = ({ chunks }) => {
180+
if (!chunks) {
181+
return {}
182+
}
183+
return filesByChunk(chunks)
184+
}
185+
175186
/** HELPERS */
176187

177188
const isUnique = (v: string, i: number, self: Files): boolean =>
@@ -189,23 +200,68 @@ const concatFilesAtKeys = (
189200
[]
190201
)
191202

203+
const filesByChunkName = (name, namedChunkGroups) => {
204+
if (!namedChunkGroups || !namedChunkGroups[name]) {
205+
return [name]
206+
}
207+
208+
return namedChunkGroups[name].chunks
209+
}
210+
211+
const hasChunk = (entry, assets, checkChunkNames) => {
212+
const result = !!(assets[entry] || assets[`${entry}-`])
213+
if (!result && checkChunkNames) {
214+
console.warn(
215+
`[FLUSH CHUNKS]: Unable to find ${entry} in Webpack chunks. Please check usage of Babel plugin.`
216+
)
217+
}
218+
219+
return result
220+
}
221+
222+
const chunksToResolve = ({
223+
chunkNames,
224+
stats,
225+
checkChunkNames
226+
}: {
227+
chunkNames: Files,
228+
stats: Object,
229+
checkChunkNames?: boolean
230+
}): Array<string> =>
231+
chunkNames
232+
.reduce((names, name) => {
233+
if (!hasChunk(name, stats.assetsByChunkName, checkChunkNames)) {
234+
return names
235+
}
236+
const files = filesByChunkName(name, stats.namedChunkGroups)
237+
names.push(...files)
238+
return names
239+
}, [])
240+
.filter(isUnique)
241+
192242
const filesFromChunks = (
193243
chunkNames: Files,
194-
assets: FilesMap,
244+
stats: Object,
195245
checkChunkNames?: boolean
196246
): Files => {
197-
const hasChunk = entry => {
198-
const result = !!(assets[entry] || assets[entry + '-'])
199-
if (!result && checkChunkNames) {
200-
console.warn(`[FLUSH CHUNKS]: Unable to find ${entry} in Webpack chunks. Please check usage of Babel plugin.`)
201-
}
247+
const chunksByID = findChunkById(stats)
202248

203-
return result
249+
const entryToFiles = entry => {
250+
if (typeof entry === 'number') {
251+
return chunksByID[entry]
252+
}
253+
return (
254+
stats.assetsByChunkName[entry] || stats.assetsByChunkName[`${entry}-`]
255+
)
204256
}
205257

206-
const entryToFiles = entry => assets[entry] || assets[entry + '-']
258+
const chunksWithAssets = chunksToResolve({
259+
chunkNames,
260+
stats,
261+
checkChunkNames
262+
})
207263

208-
return [].concat(...chunkNames.filter(hasChunk).map(entryToFiles))
264+
return [].concat(...chunksWithAssets.map(entryToFiles)).filter(chunk => chunk)
209265
}
210266

211267
/** EXPORTS FOR TESTS */

0 commit comments

Comments
 (0)