-
Notifications
You must be signed in to change notification settings - Fork 166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
More webpack updates - package size optimizations and long term caching #1126
More webpack updates - package size optimizations and long term caching #1126
Conversation
d8f66f6
to
d4cd438
Compare
It must be noted that absolute package size reduction is not that impressive mainly because full babel transpilation + some arrangements required to ensure real stable long term hashes end up bloating the package a fair bit. |
If this gets merged it's very important that somebody from the core team goes through the core-vendors configuration to make sure that those modules are really stable, both internally (i.e. you're happy with using same version of the module for a long time) and externally (i.e. the module is not going to be switched out). |
5c0dc2b
to
db615d5
Compare
db615d5
to
f0b6095
Compare
f0b6095
to
2f30aa8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really cool, and it's going to make auspice a lot better 😄
I've gone through the code and it all looks good, but have only tested briefly so far & will do more over the coming days.
In the meantime, let's get #1120 merged as this sits on top of that, and the comments there will also apply here.
babel.config.js
Outdated
"@babel/preset-env", | ||
{ | ||
useBuiltIns: "usage", | ||
targets: "cover 95%", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 this covers IE9 which is still needed for Auspice
async ensureLanguageResources(lang) { | ||
for (const ns of ["language", "sidebar", "translation"]) { | ||
if (!i18n.hasResourceBundle(lang, ns)) { | ||
const res = await import(/* webpackMode: "lazy-once" */ `../../locales/${lang}/${ns}.json`); // eslint-disable-line |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now the locales are all bundled together (thanks for the clear comment related to this). Does this "lazy-once" behavior allow for locals to each be in their own chunk if one day we stop enforcing them to be together via the wepack config?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So basically you have 3 scenarios:
-
Without an explicit locale config in webpack.config.js, and with no
webpackMode: "lazy-once"
, each locale will create its own micro-chunk. This may seem a good idea at first glance because most people are only ever going to use one locale, however what happens is that various chunk merging plugins tend to merge these micro-chunks in very unpredictable ways and you get the locales bundled up with other stuff, defeating the purpose. So I would advise against this. -
With
lazy-once
and the explicit single-package locale config (as is now), thelazy-once
is basically ignored, it only serves as a safeguard against scenario (1) above. -
If you decide to split locales further, e.g.
locales1: {
test: /\/src\/locales\//,
name: "locales-others",
enforce: true,
priority: 1,
chunks: "all"
},
locales2: {
test: /\/src\/locales\/(es|pt|fr)\//,
name: "locales-es.pt.fr",
enforce: true,
priority: 2, // This priority must be higher than the "locales-others" chunk
chunks: "all"
},
in that case you should absolutely remove the lazy-once
directive because otherwise when the locales get lazy loaded, all the separate packages will be loaded at once (even if they are in multiple chunks, all chunks will be requested at once), thus nullifying the benefit of splitting them.
/** | ||
* Here we put the libraries that are unlikely to change for a long time. | ||
* Every change or update of any of these libraries will change the hash | ||
* of the big vendor bundle, so we must be sure they're stable both internally |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When this needs changing, the bundle hash will change and so will the script src (in dist/index.html
), correct? And this means that the client will treat it as completely different request, and thus there's no danger of using the old, now invalid bundle from the cache?
If this is true, is there benefit to adding a (large) max-age
to the response for these bundles, since if their contents change the hash will also change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes all of the above is correct, I didn't check how you're caching .js files atm on the public site but basically the whole point of this setup is you set index.html to be served with no no cache whatsoever (something like no-cache,no-store,must-revalidate
), then you set nice and long (multiple months) caching for the .js part, and the hashes in the names of the scripts take care of cache busting for you.
And when you build the index.html will generate automatically with the correct hashes, modifying only those that should be modified.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! While nextstrain.org uses a custom server, I know there are other groups using auspice view
as a production server, so we should set these here. Playing around with it, it seems as simple as
- res.sendFile(path.join(auspiceBuild.baseDir, "dist/index.html"))
+ res.sendFile(path.join(auspiceBuild.baseDir, "dist/index.html"), {headers: {"Cache-Control": "no-cache, no-store, must-revalidate"}});
and
- app.use("/dist", expressStaticGzip(auspiceBuild.distDir));
+ app.use("/dist", expressStaticGzip(auspiceBuild.distDir, {maxAge: '365d'}));
but wanted to double check with you (and the nextstrain.org server uses very similar code)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for forgetting to reply here, yep it should suffice as far as I can tell
webpack.config.js
Outdated
}); | ||
const pluginHtml = new HtmlWebpackPlugin({ | ||
filename: 'index.html', | ||
template: './index.html' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the template is unusable by itself, would it be more typical to move this to src/index.html
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep I think that could be a good idea 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great -- i'm in favor of this. But currently, this doesn't build when auspice is run as a project's dependency with extensions. For instance, on nextstrain.org we get
internal/fs/utils.js:230
throw err;
^
Error: ENOENT: no such file or directory, open '/Users/naboo/github/nextstrain/nextstrain.org/node_modules/auspice/index.html'
at Object.openSync (fs.js:457:3)
at Object.readFileSync (fs.js:359:35)
at Object.exportIndexDotHtml (/Users/naboo/github/nextstrain/nextstrain.org/node_modules/auspice/cli/utils.js:112:17)
at /Users/naboo/github/nextstrain/nextstrain.org/node_modules/auspice/cli/build.js:57:15
...
I think this is relatively straightforward to fix by updating exportIndexDotHtml()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually with index.html being outputted in the /dist folder, exportIndexDotHtml() becomes completely obsolete. As far as I can see, so I think it can be killed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the interest of keeping the risk of breaking other moving parts at a minimum, it's still probably better to updated exportIndexDotHtml. This new updated version I pushed is compatible with nextstrain.org master
as well.
I added the "new" node_modules resolution from #1120 in a slightly different flavor, virtually this implementation is more robust, in practice it's gonna be the same for 99.99% of practical cases. |
6f716c5
to
167609d
Compare
167609d
to
be3ec90
Compare
6b14e8f
to
c3d4957
Compare
I also switched a couple of dependencies and devDependencies, namely putting @babel in devDependencies was causing the build to fail when auspice is used as a module |
In this latest commit I updated the bundling strategy with another tiny bit: using as entry point a micro-async entrypoint that only imports (asynchronously) the main, real entrypoint, and is hashed with [contenthash]. The reasoning for this is as follows: Now, in webpack, the URL of the initial synchronous chunks are dropped in the HTML, but the URLs of the async chunks are stored in the entrypoint (the main, non-numbered JS bundle). This means that we have this situation:
The solution: having an ant-sized entrypoint bundle (<1.5kB) that basically just contains the updated URLs of the chunks at every build, and is |
To be noted that the bundlesize test, which hardcodes the vendor bundle hashes, has been passing nicely through these latest modifications which hit both the entrypoint and the webpack configuration, so we can see that they're decently stable... ! Due to the hardcoding of the hashes in the bundlesize configuration, it means the bundlesize test will fail if for some reason the vendor bundles for long term hashing are unwittingly changed.
from the other PR. |
|
||
componentWillMount() { | ||
async componentWillMount() { | ||
if (!this.props.language || this.props.language === "en") return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not all translate keys return the same string value in english -- t(x) === x
is mostly, but not always, true. I think the current implementation deliberately doesn't load the English local, instead utilizing the fact that i18n will fall back to the provided strings. This results in errors in only a few places, for instance clicking on the "Download data" modal produces
I think an acceptable solution would be to let the English locale be bundled wherever webpack wants to put it (it's <2kb gz) but keep the other languages in a separate bundle which is only fetched when necessary (as done now).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uh, I had completely missed this. Yes I think your solution is best, I modified a tiny bit index.js and webpack.config.js to accommodate for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The resulting bundle sizes are pretty amazing and i'm really excited to get this out as it'll make a big difference.
The hot reloading ability of auspice develop
seems to have gone with this PR (it was never perfect, but it's become worse here). The bundle is still being rebuild (the server prints messages to this effect, and reloading the page reflects changes) but the browser doesn't auto-update with changes. We can potentially fix this after merge if needs must.
d09e498
to
ed4b13a
Compare
I must walk back on my initial claim that the modified |
I managed to make (most of) hot reload work again altho it's not exactly a best-in-class solution, it should suffice while we try to figure out why it does not work as expected in the first place. |
08464ac
to
a051ec2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wonderful. I'm going to take care of the cache-related headers in separate PRs (for both auspice & nextstrain.org), but this is working well in all my testing setups & represents a big step forward for auspice. Thanks again @salvatore-fxpig!
This commit (building on top of #1126) enforces that index.html is never cached, as it gets recreated when auspice rebuilds and the src for the main JS bundle changes accordingly. This may not be strictly necessary, as the max-age was 0 and the ETag changes as the file changes, but setting this explicitly is good practice. JS bundles, which use hashes to facilitate cache busting, are served with a 30d cache lifetime. Note that this is not enough to _force_ browsers to not revalidate within the 30d (i.e. 304 response) -- see [this post](https://stackoverflow.com/questions/26166433/how-to-prevent-request-that-returns-304/26339940#26339940) for context. Revalidation during the 30d lifetime will return a 304 which is no worse than the current situation we have.
This commit (building on top of #1126) enforces that index.html is never cached, as it gets recreated when auspice rebuilds and the src for the main JS bundle changes accordingly. This may not be strictly necessary, as the max-age was 0 and the ETag changes as the file changes, but setting this explicitly is good practice. JS bundles, which use hashes to facilitate cache busting, are served with a 30d cache lifetime. Note that this is not enough to _force_ browsers to not revalidate within the 30d (i.e. 304 response) -- see [this post](https://stackoverflow.com/questions/26166433/how-to-prevent-request-that-returns-304/26339940#26339940) for context. Revalidation during the 30d lifetime will return a 304 which is no worse than the current situation we have.
When running `auspice view` there is some logic to ask if we have a customised client bundle present, which we should serve, or whether we should serve the vanilla (default) auspice client. PR #1126 introduced hashes into the (client) JS bundle names, which resulted in this logic missing when there was a customised client present, so we would always fall back to the vanilla auspice client.
Description of proposed changes
The goal of this pull request is providing a stable and definitive solution to transpilation of extensions, using babel to transpile all the code (app and dependencies). This increases the build time to around 75s which is still largely tolerable, considering that rebuild time in dev mode is not affected.
while at the same time tackling the problem of package size creep by implementing a number of optimizations:
papaparse
,dompurify
,marked
,react-ga
react-select
, excluding non-used parts of the library@extensions
path to avoid the webpack import being bamboozledd3-scale
by selectively including the used moduleslodash
by applyinglodash-webpack-plugin
Related issue(s)
#1106
#902
Testing
I tested the build, with nextstrain.org extensions enabled (but running as standalone), and removing the forced include of the transpiled version of iso-639-1 from the extension code, and it displays in IE11 and Safari 5.1 (the latter with some errors, the same on current master build).
The size of the core app bundle (i.e. the bundle that is downloaded when navigating to localhost:4000/ and then picking e.g. dengue) is reduced to 390kB (from 400kB), of which around 200kB can be cached on long term (i.e. change a couple times per year) and another 50kB can be cached for mid term (i.e. change once every couple months).
To make the nexstrain repo fully compatible with the present PR, a minimal update of the code is required to make nextstrain.org serve index.html from the dist/ folder, I opened a separate (backwards-compatible) PR on nextstrain.org.
nextstrain/nextstrain.org#166
Some more cross-browser testing would probably be a good idea.
Thank you for contributing to Nextstrain!