[RFC] - Module/nomodule for NextJs #7563
Replies: 26 comments 1 reply
-
Related community issue: #3056 |
Beta Was this translation helpful? Give feedback.
-
With this approach, it would seem like only including the polyfills in the Also, it isn't mentioned in the RFC, but would be nice to allow users to override the modern build's |
Beta Was this translation helpful? Give feedback.
-
@TxHawks Yes, you are right. Looking into the polyfills bit now. I definitely want to have them removed when I get the modern build out.
That's a very good point! I will make that configurable. |
Beta Was this translation helpful? Give feedback.
-
For reference, a similar issue on Gatsby: gatsbyjs/gatsby#2114 |
Beta Was this translation helpful? Give feedback.
-
So I got the build working with BabelESMPlugin. Changes to next.config.js can be found here: azizhk/next-typescript-mdx-emotion#3 |
Beta Was this translation helpful? Give feedback.
-
Ralph actually already has a working branch that he’s going to push up soon. He’ll share all the details once that is up, but it uses a different approach 👍🏻 |
Beta Was this translation helpful? Give feedback.
-
Hey, When writing https://github.com/jovidecroock/webpack-module-nomodule-plugin I noticed that module-nomodule building will result in the following:
This in my opinion is "acceptable" in terms that you are supporting the older browsers and rewarding the newer ones. Just thought to put this out there, glad to see how much effort is being put into this all. Great work on Next9 big congratulations on the launch! |
Beta Was this translation helpful? Give feedback.
-
I'm not sure if that has already been considered but Nuxt.js has a |
Beta Was this translation helpful? Give feedback.
-
Differential Loading in Next.jsTL;DR: we'd like to serve modern JavaScript to ES Modules-supporting browsers but fall back to transpiled JavaScript, using the module/nomodule pattern. Part 1: Add the Safari WorkaroundMost of the issues noted above come down to fetching unnecessary resources, or duplicate copies of resources. Safari 10.1, however, actually executes both the modern and legacy scripts - that's not a performance tradeoff, it's a bug. Safari 10.1 usage is almost non-existent at this point, but the workaround is relatively small and unobtrusive. Ideally, it'd be possible to control whether this workaround is included through Next's configuration. Failing that, I think it's probably safest to include the workaround for now. Part 2: Consider a JS variant of module/nomoduleAs described in my recent post, there are fewer issues to be dealt with when loading module VS class scripts from JavaScript than from Here's an approximation of Next's HTML today: <head>
<link rel=preload as=script href=runtime.js>
<link rel=preload as=script href=page1.js>
<script src=runtime.js></script>
<script src=page1.js></script>
</head> As you can see, there's not much value in the script tags from a preloading standpoint. They are just there to trigger script execution. Here's the output approximation after Ralph's PR: <head>
<link rel=preload as=script href=runtime.esm.js>
<link rel=preload as=script href=runtime.js>
<link rel=preload as=script href=page1.esm.js>
<link rel=preload as=script href=page1.js>
<script type=module src=runtime.esm.js></script>
<script nomodule src=runtime.js></script>
<script type=module src=page1.esm.js></script>
<script nomodule src=page1.js></script>
</head> Firstly, it seems like we could make a case for not preloading those legacy bundles. The vast majority of browsers will be downloading but never executing them. Second, that's a fair bit of duplication! If we switched to a simple JavaScript-based script loader, we could do away with the duplication, but we Option: Direct Implementation of $loadjsTaking the ESM example from above, we can factor out the loading logic into a JS function: <head>
<!-- ignore preloading for now -->
<script>
// here’s our little script loader.
function $loadjs(src, fallback, s) {
s = document.createElement('script');
// if there’s nomodule support, load the modern version:
if ('noModule' in s) s.type = 'module', s.src = src;
else s.src = fallback; // otherwise give it the legacy fallback
document.head.appendChild(s);
}
// now, we just have to output function calls for each script asset:
$loadjs("runtime.esm.js", "runtime.js");
$loadjs("page1.esm.js", "page1.js");
</script>
</head> This could be extended further, since the URLs of the modern and legacy bundles always follow the same pattern - we can generate the modern bundle URL from the legacy one: <script>
function $loadjs(src, s) {
s = document.createElement('script');
if ('noModule' in s) s.type = 'module';
else src = src.replace('.esm', '');
s.src = src;
document.head.appendChild(s);
}
$loadjs('runtime.esm.js');
$loadjs(page1.esm.js');
</script> If it's determined that optimizing for the 95% case is the best way forward, adding <link rel=preload> back completes this solution and brings back the benefits of the preload scanner. It also could make it possible to automatically inject script tags for all the preload meta tags, avoiding duplication altogether: <head>
<link rel=preload as=script href=runtime.esm.js crossorigin 🐾>
<link rel=preload as=script href=page1.esm.js crossorigin 🐾>
<script>
[].map.call(document.querySelectorAll('[🐾]'),function(l,s){
s=document.createElement('script');
s.src = 'noModule' in s ? l.href : l.href.replace('.esm', '');
l.parentNode.appendChild(s);
})
</script> I'm not saying using a paw print as the sigil for this would be advisable, it just seemed cute. |
Beta Was this translation helpful? Give feedback.
-
👍 @developit's solution. Just adding a few points which would also need some thought.
|
Beta Was this translation helpful? Give feedback.
-
This is a complex problem and I like @developit's solution and tradeoff. The tradeoff is that some browsers will download both bundles because they support preload, but don't support Browsers that support preload: https://caniuse.com/#search=preload From what I can tell, only Chrome 50-60 and Opera 37-47 supports preload without also supporting I downloaded and crunched some numbers from http://gs.statcounter.com/browser-version-market-share and if I did it correctly, the global market share for those browsers: Last year: 0.98% I might have missed something, but seems like this solution is optimizing for the 99.35%. 😄 Only way I can see of improving that number further is using UA-sniffing serverside where possible (that is, only for uncached pages) to determine what script to preload. To me this seems very complex and brittle and not worth it considering the low impact. |
Beta Was this translation helpful? Give feedback.
-
I don't think "ESM Support === No Polyfills or transforms" is necessarily a great assumption. Drawing the line between "modern browsers" and "legacy browsers" is by no means black and white, and I'm not sure there's a single feature that can safely serve as the dividing-line. Safari 10 is a notable example, as it supports ES Modules, but has a broken I noted in another PR that this specific issue can have a measurable impact on bundle sizes when |
Beta Was this translation helpful? Give feedback.
-
Should we close this as #7704 is merged and released on 9.1 now? |
Beta Was this translation helpful? Give feedback.
-
It has not been released yet! 🔜 |
Beta Was this translation helpful? Give feedback.
-
then I'd say it's quite confusing to include it in a release blog post 😳 |
Beta Was this translation helpful? Give feedback.
-
@neo the feature is available as preview in the 9.1 release. Meaning, you can opt-into it if you want to test it out. We don't consider it stable enough for everyone yet. // next.config.js
module.exports = {
experimental: { modern: true }
} |
Beta Was this translation helpful? Give feedback.
-
@neo this all falls under the coming soon section of the blogpost: https://nextjs.org/blog/next-9-1#coming-soon |
Beta Was this translation helpful? Give feedback.
-
Worth noting that https://github.com/babel/preset-modules reduces the down-compiling necessary for the module/nomodule pattern (though the plan is to merge it into preset-env eventually). |
Beta Was this translation helpful? Give feedback.
-
Worth noting that preset-modules was spearheaded by the team at Google that works with us specifically for this RFC and that the experimental modern flag already uses it. |
Beta Was this translation helpful? Give feedback.
-
Nice, I forgot about that! On that topic, since you're on the inside track, has there been any work to get bundlers to standardize on a pattern library authors could use to signal similar build outputs? module/nomodule is great for next and app code, but a huge amount of our JS bundle comes from 3rd-party libraries. I looked all through webpack/rollup/etc and there's no mention of it; it seems like node's upcoming conditional exports could be the perfect thing though. |
Beta Was this translation helpful? Give feedback.
-
@developit can share more about that I think. |
Beta Was this translation helpful? Give feedback.
-
Have a few things to ask/point out. Please, correct if I'm wrong somewhere. babel helpersIt was said that babel helpers can rely on modern api, which makes it unsafe to leave their code unpolyfilled. Does next.js provide a solution for this? polyfill management without preset-env/corejsLet's assume we added some polyfills to an app in current iteration. Over the time, the support will get better and we won't need those polyfills anymore. This means, to eliminate dead code, we should check current support coverage by some automation tool or by hand. I personally feel this is not a good thing. Also I want to point out that having an automated polyfill management can be a convenient/preferred way for some projects/developers, so, please, leave this feature opt-in, as the rfc says (cause it forces Another thing that may be important for some people is polyfilling for not popular browsers. // `esmodules` as a target indicates the specific set of browsers supporting ES Modules.
// These values OVERRIDE the `browsers` field.
if (inputTargets.esmodules) {
const supportsESModules = browserModulesData["es6.module"];
browsers = Object.keys(supportsESModules)
.map(browser => `${browser} ${supportsESModules[browser]}`)
.join(", ");
} The execution of the above lines results in the following set of browsers:
There is no browsers like There also can be problems with wrong browser implementation for some features like the one mentioned in the comment. |
Beta Was this translation helpful? Give feedback.
-
Question: with this turned on, do we import libraries' cjs or (es)mjs version?
(@Timer please do let me know if this is being discuss somewhere else, thank you! 🙏 ) |
Beta Was this translation helpful? Give feedback.
-
Hi, is this still under works/discussed somewhere else? |
Beta Was this translation helpful? Give feedback.
-
Any progress on this issue? Is |
Beta Was this translation helpful? Give feedback.
-
after 3 years, any good news on this? |
Beta Was this translation helpful? Give feedback.
-
WIP
I have these changes working on my branch. I am actively testing the changes with different sites and also trying to improve the build size. I will raise a PR soon, but would love to get any feedback about the approach.
Goal
Ship modern JS to modern browsers.
Is your feature request related to a problem? Please describe.
next build
compiles all client side JS with a default babel-preset-env options, which transpiles all ECMAScript 2015+ code. While this ensures support for older browsers, it ends up shipping unnecessary Babel helpers and polyfills to modern browsers.Transpiling JS to target modern browsers will have a significant impact on bundle size and performance.
Describe the solution you'd like
Generate modern JS files as part of the current build, by compiling them with babel-preset-env options:
With this option, Babel transpiles files to target a modern browser set and generates relatively modern code.
We can then conditionally ship modern/legacy bundle with the module/nomodule pattern. (Read this blog for more info). The module/nomodule pattern provides a convenient and reliable mechanism for serving modern JavaScript to modern browsers while still allowing older browsers to fall back to polyfilled ES5.
<script type="module" src="modern-build.js">
- to load the modern files<script nomodule src="current-build.js">
- fallback for browsers that don’t support the modern features.Integrating with NextJS
Create a plugin similar to BabelESMPlugin that integrates into Nextjs and outputs two versions of bundles on build, a default bundle and a modern bundle. This plugin spawns an additional child compiler inside webpack and re-runs the entire build with the modern babel config options.
Optional feature
This feature will be enabled behind a flag in
next.config.js
. The flag would bemodern: true
to enable andmodern: false
to disable.Things to note
I built and tested this plugin locally with a couple of NextJs sites (built using NextJS). Compiling two versions of bundles increase the overall build time slightly. I noticed an average increase of
15-20%
in build time. But in general, the build time increase varies a lot based on the project's build config and its size/structure.Some stats on the before/after bundle sizes
While the size wins from modern compilation might seem small from the table, there are a couple of factors that we need to consider
Parallel work - Some additional optimizations that we are looking into
Removing/move polyfills from the modern build. NextJs by default ships polyfills like fetch and regenerator-runtime, these can be safely removed from the modern build.
node_modules source - We are working to have node_modules (atleast the ones NextJs directly depends on for now) to have them publish non-transpiled code as ESmodules. This gives us a lot of flexibility to choose the level of tranpilation needed for our modern bundle. Further/better optimization can be performed on these modern code such as better dedup, and tree-shaking, etc.
Investigation into using useBuiltIns: usage for modern build
Overall, having a modern bundle unlocks a lot of new optimization opportunities that can be further explored.
Beta Was this translation helpful? Give feedback.
All reactions