14

I'm recently working on some website optimization works, and I start using code splitting in webpack by using import statement like this:

import(/* webpackChunkName: 'pageB-chunk' */ './pageB')

Which correctly create the pageB-chunk.js, now let's say I want to prefetch this chunk in pageA, I can do it by add this statement in pageA:

import(/* webpackChunkName: 'pageB-chunk' */ /* webpackPrefetch: true */ './pageB')

Which will result in a

<link rel="prefetch" href="pageB-chunk.js">

being append to HTML's head, then the browser will prefetch it, so far so good.

The problem is the import statement I use here not just prefetch the js file, but also evaluate the js file, means the code of that js file is parsed & compile to bytecodes, the top-level code of that JS is executed.

This is a very time-consuming operation on a mobile device and I want to optimize it, I only want the prefetch part, I don't want the evaluate & execute part, because later when some user interactions happen, I will trigger the parsing & evaluate myself

enter image description here

↑↑↑↑↑↑↑↑ I only want to trigger the first two steps, pictures come from https://calendar.perfplanet.com/2011/lazy-evaluation-of-commonjs-modules/ ↑↑↑↑↑↑↑↑↑

Sure I can do this by adding the prefetch link myself, but this means I need to know which URL I should put in the prefetch link, webpack definitely knows this URL, how can I get it from webpack?

Does webpack have any easy way to achieve this?

AmerllicA
  • 29,059
  • 15
  • 130
  • 154
小广东
  • 1,151
  • 12
  • 20
  • `if (false) import(…)` - I doubt webpack does dead code analysis? – Bergi Jan 30 '20 at 08:11
  • Where/when *do* you actually want to evaluate the module? That's where the dynamic `import` code should go. – Bergi Jan 30 '20 at 08:13
  • I'm so confused now. Why the evaluation is important? because at last, the JS file should be evaluated by the client browser device. Or I don't understand the question correctly. – AmerllicA Feb 03 '20 at 22:41
  • @AmerllicA eventually yes the js should be evaluated, but think this case: My website got A, B two pages, visitors in page A often visit page B after they "done some works" on page A. Then it's reasonable to prefetch page B's JS, but if I can control the time that this B's JS is evaluated, I can 100% sure that I don't block the main thread which create glitches when visitor is trying to "done their works" on page A. I can evaluate B's JS after visitor click on a link that's point to page B, but at that time the B's JS is most likely downloaded, I just need spend a little time to evaluate it. – 小广东 Feb 04 '20 at 01:12
  • Sure according to chrome v8's blog: https://v8.dev/blog/cost-of-javascript-2019, they done a lot optimizations to achieve the blazing fast JS parsing time, by utilizing Worker thread and many other techs, details in here https://www.youtube.com/watch?v=D1UJgiG4_NI. But other browsers doesn't implement such optimization yet. – 小广东 Feb 04 '20 at 01:19
  • Another problem with webpack is it's prefetch doesn't make sense at most of time, it gather all ur /* webpackPrefetch: true */ comments in ur chunk, and when ur chunk loaded, it prefetch them all.... This may work for Desktop, but definitely not for mobile, mobile user need to pay for their data, prefetch something they never used is unreasonable. I need to take control when to prefetch something or not. – 小广东 Feb 04 '20 at 01:23

3 Answers3

4

UPDATE

You can use preload-webpack-plugin with html-webpack-plugin it will let you define what to preload in configuration and it will automatically insert tags to preload your chunk

note if you are using webpack v4 as of now you will have to install this plugin using preload-webpack-plugin@next

example

plugins: [
  new HtmlWebpackPlugin(),
  new PreloadWebpackPlugin({
    rel: 'preload',
    include: 'asyncChunks'
  })
]

For a project generating two async scripts with dynamically generated names, such as chunk.31132ae6680e598f8879.js and chunk.d15e7fdfc91b34bb78c4.js, the following preloads will be injected into the document head

<link rel="preload" as="script" href="chunk.31132ae6680e598f8879.js">
<link rel="preload" as="script" href="chunk.d15e7fdfc91b34bb78c4.js">

UPDATE 2

if you don't want to preload all async chunk but only specific once you can do that too

either you can use migcoder's babel plugin or with preload-webpack-plugin like following

  1. first you will have to name that async chunk with help of webpack magic comment example

    import(/* webpackChunkName: 'myAsyncPreloadChunk' */ './path/to/file')
    
  2. and then in plugin configuration use that name like

    plugins: [
      new HtmlWebpackPlugin(),   
      new PreloadWebpackPlugin({
        rel: 'preload',
        include: ['myAsyncPreloadChunk']
      }) 
    ]
    

First of all let's see the behavior of browser when we specify script tag or link tag to load the script

  1. whenever a browser encounter a script tag it will load it parse it and execute it immediately
  2. you can only delay the parsing and evaluating with help of async and defer tag only until DOMContentLoaded event
  3. you can delay the execution (evaluation) if you don't insert the script tag ( only preload it with link)

now there are some other not recommended hackey way is you ship your entire script and string or comment ( because evaluation time of comment or string is almost negligible) and when you need to execute that you can use Function() constructor or eval both are not recommended


Another Approach Service Workers: ( this will preserve you cache event after page reload or user goes offline after cache is loaded )

In modern browser you can use service worker to fetch and cache a recourse ( JavaScript, image, css anything ) and when main thread request for that recourse you can intercept that request and return the recourse from cache this way you are not parsing and evaluating the script when you are loading it into the cache read more about service workers here

example

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('v1').then(function(cache) {
      return cache.addAll([
        '/sw-test/',
        '/sw-test/index.html',
        '/sw-test/style.css',
        '/sw-test/app.js',
        '/sw-test/image-list.js',
        '/sw-test/star-wars-logo.jpg',
        '/sw-test/gallery/bountyHunters.jpg',
        '/sw-test/gallery/myLittleVader.jpg',
        '/sw-test/gallery/snowTroopers.jpg'
      ]);
    })
  );
});

self.addEventListener('fetch', function(event) {
  event.respondWith(caches.match(event.request).then(function(response) {
    // caches.match() always resolves
    // but in case of success response will have value
    if (response !== undefined) {
      return response;
    } else {
      return fetch(event.request).then(function (response) {
        // response may be used only once
        // we need to save clone to put one copy in cache
        // and serve second one
        let responseClone = response.clone();

        caches.open('v1').then(function (cache) {
          cache.put(event.request, responseClone);
        });
        return response;
      }).catch(function () {
        // any fallback code here
      });
    }
  }));
});

as you can see this is not a webpack dependent thing this is out of scope of webpack however with help of webpack you can split your bundle which will help utilizing service worker better

Tripurari Shankar
  • 3,308
  • 1
  • 15
  • 24
  • but still my pain point is I can't get the file's url easily from webpack, even I go with SW, I still need to let SW know what files should be pre-cache... a webpack manifest plugin can generate manifest info into SW, but it's an all in operation, means SW have no choice but pre-cache all files listed in manifest... – 小广东 Feb 03 '20 at 03:50
  • Ideally, I hope webpack can add another magic comment like /* webpackOnlyPrefetch: true */, so I can call import statement twice for every lazy loadable chunk, one for prefetch, one for code evaluation, and everything happen on demand. – 小广东 Feb 03 '20 at 03:53
  • 1
    @migcoder that's a valid point ( you can't get filename because that's generated dynamically on runtime ) will look into any solution if i can find any – Tripurari Shankar Feb 03 '20 at 04:02
  • @migcoder I have updated the answer please see it that solves your problem – Tripurari Shankar Feb 03 '20 at 04:13
  • it solve part of the problem, it can filter out the async chunks that's good, but my final goal is only prefetch demanded async chunks. I'm currently looking at this plugin https://github.com/sebastian-software/babel-plugin-smart-webpack-import, it shows me how to gather all import statements and do some code modification base on the magic comments, maybe I can create a similar plugin to insert the prefetch code on import statements with 'webpackOnlyPrefetch: true' magic comment. – 小广东 Feb 03 '20 at 04:38
  • that sounds great. meanwhile i will search if something like this exist in webpack plugin world – Tripurari Shankar Feb 03 '20 at 06:36
  • maybe you can try `/* webpackChunkName: 'pageB-chunk' */` and in plugin configuration use `include: ['pageB-chunk']` – Tripurari Shankar Feb 03 '20 at 06:39
  • I post my answer blow, babel-plugin is quite hacky but doable, again thanks for ur helps on this, bountry is urs~:P – 小广东 Feb 04 '20 at 04:46
  • Awesome work @migcoder . also i tried preloading only one specific async chunk with help of `preload-webpack-plugin` and it worked first you have to name that async chunk with magic comment example `import(/* webpackChunkName: 'myAsyncPreloadChunk' */ './path/to/file')` and once you have named it you can use plugin configration of `include: ['myAsyncPreloadChunk']` and it works like a charm will update the answer – Tripurari Shankar Feb 04 '20 at 07:28
3

Updates: I include all the things into a npm package, check it out! https://www.npmjs.com/package/webpack-prefetcher


After few days of research, I end up with writing a customize babel plugin...

In short, the plugin work like this:

  • Gather all the import(args) statements in the code
  • If the import(args) contains /* prefetch: true */ comment
  • Find the chunkId from the import() statement
  • Replace it with Prefetcher.fetch(chunkId)

Prefetcher is a helper class that contain the manifest of webpack output, and can help us on inserting the prefetch link:

export class Prefetcher {
  static manifest = {
    "pageA.js": "/pageA.hash.js",
    "app.js": "/app.hash.js",
    "index.html": "/index.html"
  }
  static function fetch(chunkId) {
    const link = document.createElement('link')
    link.rel = "prefetch"
    link.as = "script"
    link.href = Prefetcher.manifest[chunkId + '.js']
    document.head.appendChild(link)
  }
}

An usage example:

const pageAImporter = {
  prefetch: () => import(/* prefetch: true */ './pageA.js')
  load: () => import(/* webpackChunkName: 'pageA' */ './pageA.js')
}

a.onmousehover = () => pageAImporter.prefetch()

a.onclick = () => pageAImporter.load().then(...)

The detail of this plugin can found in here:

Prefetch - Take control from webpack

Again, this is a really hacky way and I don't like it, if u want webpack team to implement this, pls vote here:

Feature: prefetch dynamic import on demand

小广东
  • 1,151
  • 12
  • 20
  • There is a problem that the file name generated may be not the same as you write in manifest,such as app.[contentName].js – gwl002 Aug 13 '21 at 08:32
0

Assuming I understood what you're trying to achieve, you want to parse and execute a module after a given event (e.g click on a button). You could simply put the import statement inside that event:

element.addEventListener('click', async () => {
  const module = await import("...");
});
Eliya Cohen
  • 10,716
  • 13
  • 59
  • 116