Developer Frontend Love

The future of JavaScript bundlers.

Photo by azhrjl on Unsplash
Image of Johannes Ewald
Peerigon logo
Webpack logo

Disclaimer.

The opinions in this presentation are my own. This is not a statement from the webpack team.

Do we even need JavaScript bundlers?

Problems with JavaScript bundlers:

  • Suddenly the web needs a build step.
  • Configuration hell.
  • The code you write is not the code that is executed.

You have to deal with problems you didn't have before.

Why isn't everyone just using ES modules?

Image of a resolve module error

No bare module specifiers:


                    

No optional file extensions:


                    

No package.json resolving:


                    

So, will we still need JavaScript bundlers in the future?

The future of JavaScript bundlers.

What's the purpose of JavaScript bundlers today?

Heads up!

The following proposals are in very early stages. Take them with a grain of salt.

Dev experience

JS modules.

We don't need JS bundlers for that, it's already possible with ESM.

But: our current problem is resolving.

Things we can do: not rely on "cute" (Ryan Dahl) resolving features.

  • Use file extensions.
  • Use the .mjs extension for JavaScript modules once ESMs land in Node.js.
  • Avoid index.js.
  • Avoid module aliases just for ergonomic reasons.
Just use the real path.

But do we really have to give up bare module specifiers (BMS)?

Advantages of BMS:

  • Shorter imports for common packages.
  • Indirection at package-level which allows the main/browser switch. This is important for isomorphic modules like isomorphic-fetch.
  • They hide the actual file layout of third-party modules.

                    

There is an intent to implement from the Blink team.

Dev experience

HTML, CSS & WASM modules.

As a replacement for HTML imports:


                        

                    

                        

                    

There is an intent to implement from the Edge team.

CSS modules export a Constructable Stylesheet:


                    

The goal: Turn this imperative API...


                    

...into a declarative API:


                    

Challenge: Allow cyclic dependencies between non-JS modules.

Source: Lin Clark's presentation about WebAssembly ES module integration.

Cyclic dependencies between two WASM modules probably won't work at all.

Source: Lin Clark's presentation about WebAssembly ES module integration.
Dev experience

Custom languages.

Has been stopped and broken down into smaller pieces like:


                    

Interesting proposal: Asset references


                    

Another interesting proposal: import as-proposal


                    

Advantages of these proposals:

  • No out-of-band configuration of loaders. Modules determine how they want to import a dependency.
  • Custom language support for NPM modules. Sharing frontend components would become easier.
  • One step closer to #0CJS (zero config).

One thing to note:

These are proposals to do the resource transformation on run time.

In order to get it fast for production, you would probably want to move this transformation to build time.

Hence, the transformation should be statically analyzable.

This is kind of hard to standardize because bundlers, Node.js and browsers have very different requirements.

But there is an interesting alternative to loaders...

Babel macros is a Babel plugin that allows you to evaluate code on build time.


                    

                    

How can this substitute loaders?


                    

                        

A good example of why modules should be able to configure their own loaders.

But how would that work without Babel?

With a package switch:


                    

Current problem of Babel macros:

They require synchronous I/O on build and on run time.

Dev experience

Short feedback loop.

  • We do not need a build step at all if we can do the transformations on run time.
  • But: we would need a way to make the transformations cacheable.
  • For HMR, we would still need a dev server with WebSockets.
Compatibility

Unified module systems.

Authors will slowly adopt the common denominator:

  • ESM.
  • Imports with explicit file extensions.
  • Using the .mjs file extension for ESMs.

Bundlers won't be necessary because of compatibility reasons.

Compatibility

Unified platform.

My hope: Platform independent APIs will converge some day.

Good example: the URL constructor.


                    

Bad example: fetch().


                    
Compatibility

New language features.

Will be less relevant with evergreen browsers and incremental language updates, but probably still necessary for production builds.

Also polyfills still require a lot of manual configuration.

Idea: Automatic polyfills.

What if library authors annotated what polyfills they need?


                        
Optimization

Optimized resource loading.

Should we even create chunks with ESMs and H2 push?

Push is a new HTTP feature that allows the server to push resources pro-actively to the client.

Photo by Wolfgang Hasselmann on Unsplash
There is no clear answer (yet) and many factors need to be considered.

Current problems with ESM and H2 push:

  • Browsers haven't optimized native modules yet.
  • Inconsistent H2 push implementations.
  • Pushing resources that the client has already cached is worse than no push.
  • Text compression algorithms (like gzip) are more effective on bigger files.

H2 push is still hard.

Rules of thumb for H2 push.

As implementations mature, we need to re-evaluate the question whether we should create chunks or not.

Let's assume that we really, really wanted to use H2 push.

How would it work?

There are two possible solutions:

  • Use an empiric approach like node-h2-auto-push which tries to learn "what to push" by observing client requests or...
  • ...use a bundler which generates a dependency graph on build time and pass the result to the H2 server.
  • Creating a dependency graph for H2 push is not the only performance optimization a bundler can do.

    But the problem is: Getting the best performance requires a lot of configuration.

    The solution: Give more data to the bundler so that it can perform automated decisions.

    Data source #1

    Static analysis.

    It could add resource hints automatically, like preconnect for all hosts that have been parsed from the source code.

    
                        

    Example: webpackPrefetch: true

    
                            
                                Since webpack 4.6.0.
                            
                        

    Another idea: webpackPreload: true for render critical fonts.

    
                            
                                Does not work in CSS files yet.
                            
                        

    But these manual hints can get outdated over time.

    Data source #2

    Automated critical render path tests.

    The idea:

    • There is an automated test that navigates to popular routes.
    • The bundler captures relevant performance data.
    • The bundler decides which resources to optimize and to prioritize.

    Example: Create optimized subsets of fonts based on the actual usage of glyphs.

    Another example: Inline render critical CSS.

    • This is already a popular feature of CSS-in-JS libraries.
    • critical

    Running automated critical render path tests works especially well with the JAMstack.

    But these automated tests are also error prone because we have to select popular routes.

    What if instead of artifical usage data we used...

    ...REAL USAGE DATA.
    Data source #3

    Analytics.

    Data-driven user experiences: When real usage data drives the decisions about how to optimize the app.

    And this is already possible today...

    Guess.js...

    • ...uses real usage data to decide which chunk to prefetch next.
    • ...uses probabilities combined with navigator.connection.effectiveType to avoid over-fetching and thus wasting expensive data on mobile.
    Optimization

    Long-term caching.

    Calculating content hashes and updating URLs will always require a bundler.

    But there's another problem: In order to avoid propagating content hashes, we need to use an out-of-band configuration.

    Example:

    
                            
    
                            

    A single change invalidates all content hashes.

    Possible solution:

    • Do not use hashes in import paths.
    • Re-map imports via import maps.
      
                                  
    Optimization

    Smaller file sizes.

    All these optimizations are still necessary:

    • Tree-shaking.
    • Dead-code elimination.
    • Minification.
    • Compression.

    This is the reason why you will always have a build step for high-performance websites.

    Conclusion.

    Bundlers were invented to use Node.js modules in the web, but this has changed.

    Today, we use bundlers for a lot of reasons, like dev experience, compatibility and optimizations.

    Will we still need bundlers in the future?

    = Bundler not necessary
    = Bundler probably not necessary
    = Bundler necessary in production

    Yes, because bundlers can do stuff on build time that would otherwise need to be done on run time.

    But the scope of bundlers will probably change.

    My vision:

    • Bundlers shouldn't be necessary because of compatibility reasons.
    • Bundlers shouldn't be necessary to allow us to write the code the way we want it.
    • They should only be necessary to make our apps fast.

    In order to make that happen without configuration overhead, we need to give bundlers more insights into our application...

    • via static analysis,
    • via automated critical render path tests,
    • via real usage data.

    Thank you!

    @jhnnns

    🙇