On webpack how can I import a script without evaluate it?

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


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