Skip to content
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

Webpack 2 Dynamic Import degrades build performance (slow) compared to static imports. #4636

Closed
AustinHunt opened this issue Apr 4, 2017 · 19 comments

Comments

@AustinHunt
Copy link

AustinHunt commented Apr 4, 2017

Do you want to request a feature or report a bug?
I would like to report a bug.

What is the current behavior?
When converting my project's components to dynamically import components instead of using static import statements, the build time increases by 10x and incremental builds by about the same.

The dynamic import functionality has reduced my bundle size astronomically by breaking all of my react components into separate chunks that get loaded into the app on-demand. I used a react-loader setup to replace most of my component static imports with dynamic imports.

If the current behavior is a bug, please provide the steps to reproduce.

  1. Create a large number static imports in your project.
  2. Convert those imports into dynamic imports.
  3. Run webpack-dev-server with hot-reloading for both and compare the build time.
//Static Example: index.js
import ComponentA from './component-a';
import ComponentB from './component-b';

export { ComponentA, ComponentB }

//Dynamic Example: index.js
import Loadable from 'react-loadable';

const LoadingComponent = f => {
  return (
    <div style={{textAlign:'center'}}><i className="fa fa-spinner fa-spin fa-2x"></i></div>
  )
}

const LoadableComponent = opts => {
  return Loadable({
    delay: 300,
    LoadingComponent,
    ...opts,
  })
}

const ImportLoadable = (loader) => {
  const Component = LoadableComponent({
    loader
  });

  return Component;
}

const ComponentA = ImportLoadable(f => import('./component-a'));
const ComponentB = ImportLoadable(f => import('./component-b'));

export { ComponentA, ComponentB }

What is the expected behavior?
I expect the dynamic import build time to be the same as a static import build time.

If this is a feature request, what is motivation or use case for changing the behavior?
It seems that even though the dynamic import automatically chunks your bundle into multiple files, that when changing the underlying code for one of those chunks, webpack should be able to detect which file that was and only rebuild that file and hot-reload it.

Please mention other relevant information such as the browser version, Node.js version, webpack version and Operating System.
Webpack 2.3.2
Webpack-Dev-Server 2.4.2

@AustinHunt
Copy link
Author

AustinHunt commented Apr 4, 2017

As a side note, I couldn't find an exact replica issue for this bug. #4582 seems to be a similar problem?

@AustinHunt
Copy link
Author

AustinHunt commented Apr 4, 2017

Temporary workaround we found:

import { ImportLoadable } from '../';
let Account;
let AccountContainer;
let AddCard;
let EditCard;
let DeleteCard;
let CardForm;
let Cards;
let Card;
let Charges;
let Charge;
let Invoices;
let Invoice;
let PurchaseMap;
let PurchasePlan;

if(process.env.NODE_ENV == 'production') {
	Account = ImportLoadable(f => import('./account'));
	AccountContainer = ImportLoadable(f => import('./accountContainer'));
	AddCard = ImportLoadable(f => import('./addCard'));
	EditCard = ImportLoadable(f => import('./editCard'));
	DeleteCard = ImportLoadable(f => import('./deleteCard'));
	CardForm = ImportLoadable(f => import('./cardForm'));
	Cards = ImportLoadable(f => import('./cards'));
	Card = ImportLoadable(f => import('./card'));
	Charges = ImportLoadable(f => import('./charges'));
	Charge = ImportLoadable(f => import('./charge'));
	Invoices = ImportLoadable(f => import('./invoices'));
	Invoice = ImportLoadable(f => import('./invoice'));
	PurchaseMap = ImportLoadable(f => import('./purchaseMap'));
	PurchasePlan = ImportLoadable(f => import('./purchasePlan'));
}
else {
	Account = require('./account');
	AccountContainer = require('./accountContainer');
	AddCard = require('./addCard');
	EditCard = require('./editCard');
	DeleteCard = require('./deleteCard');
	CardForm = require('./cardForm');
	Cards = require('./cards');
	Card = require('./card');
	Charges = require('./charges');
	Charge = require('./charge');
	Invoices = require('./invoices');
	Invoice = require('./invoice');
	PurchaseMap = require('./purchaseMap');
	PurchasePlan = require('./purchasePlan');
}

export {
	Account,
	AccountContainer,
	AddCard,
	EditCard,
	DeleteCard,
	CardForm,
	Cards,
	Card,
	Charges,
	Charge,
	Invoices,
	Invoice,
	PurchaseMap,
	PurchasePlan
}

UDPATE: ^^^This actually did not work.

@sokra
Copy link
Member

sokra commented Apr 4, 2017

do you have a repo for profiling?

@Vanuan
Copy link

Vanuan commented Apr 8, 2017

I have this too. ~50 chunks with require.ensure results in increase of build time from 120 seconds to 500 seconds which is almost 10 minutes!

Does it mean that each entrypoint results in a fresh evaluation of its dependency tree?

@AustinHunt
Copy link
Author

I have a shallow understanding of webpack, so I'm unsure. I would make a pull request with the change if I can be pointed in the right direction to fix the issue.

@Vanuan
Copy link

Vanuan commented Apr 10, 2017

I've tried this syntax and it's much better:

               require.ensure(['./container'], require => {
                       cb(null, require('./container').default);
               })

@sokra
Copy link
Member

sokra commented Apr 11, 2017

do you have a repo for profiling?

@niieani
Copy link
Contributor

niieani commented Apr 16, 2017

Related to #4716

@peterdotjs
Copy link

@Vanuan a temporary workaround I've used is to strip out require.ensure during development. using https://github.com/knpwrs/babel-plugin-remove-webpack as noted in #4716. One caveat is that this stripping out needs to be done outside of Webpack through babel command line.

@AustinHunt
Copy link
Author

@peterdotjs looks like babel-plugin-remove-webpack does not in-fact remove webpack 2 dynamic import syntax. See docs

@peterdotjs
Copy link

@NomadGraphix I was referring specifically to require.ensure but the same general technique (with a different plugin) could be used for the dynamic import syntax that you specified.

@ingro
Copy link

ingro commented May 16, 2017

I'm facing this issue too, converting a lot of require.ensure calls to use the new dynamic import crippled down massively the building time.

@ingro
Copy link

ingro commented May 16, 2017

I've created a small repo to help reproduce the problem here.

Just clone and npm install, then you can run two commands:

  • npm run import: run the web server using dynamic imports;
  • npm run require: run the web server using require.ensure syntax

The repo and the files included are very small so the difference is not that big but if you modify a random file it's usually taking ~300/400ms to rebuild with require.ensure versus ~700/800ms with import (the problem is way more evident in real apps).

Also if you edit a file with require.ensure here's the output:

webpack: Compiling...
Hash: 01801f65cbafafb802d7
Version: webpack 2.5.1
Time: 442ms
                                   Asset       Size  Chunks                    Chunk Names
                           0.requires.js    2.92 MB       0  [emitted]  [big]
                             requires.js     346 kB       1  [emitted]  [big]  main
    0.3e7d4048a4682840be2d.hot-update.js  844 bytes       0  [emitted]
    3e7d4048a4682840be2d.hot-update.json   43 bytes          [emitted]
                       0.requires.js.map    3.08 MB       0  [emitted]
0.3e7d4048a4682840be2d.hot-update.js.map  478 bytes       0  [emitted]
                         requires.js.map     408 kB       1  [emitted]         main
webpack: Compiled successfully.

versus this using import:

webpack: Compiling...
Hash: 8bbc15d48815f50afd15
Version: webpack 2.5.1
Time: 747ms
                                    Asset       Size  Chunks                    Chunk Names
     d71b0f87e6ade357067e.hot-update.json   44 bytes          [emitted]
                             0.imports.js    1.96 MB       0             [big]
                             2.imports.js    1.49 MB       2             [big]
                             3.imports.js    1.49 MB       3             [big]
                             4.imports.js    1.49 MB       4             [big]
                             5.imports.js    1.49 MB       5             [big]
                             6.imports.js    1.49 MB       6             [big]
                             7.imports.js    1.49 MB       7             [big]
                             8.imports.js    1.49 MB       8             [big]
                             9.imports.js    1.49 MB       9             [big]
                            10.imports.js    1.49 MB      10             [big]
                            11.imports.js    1.49 MB      11             [big]
                            12.imports.js    1.49 MB      12             [big]
                            13.imports.js    1.49 MB      13             [big]
                            14.imports.js    1.49 MB      14             [big]
                            15.imports.js    1.49 MB      15             [big]
                            16.imports.js    1.13 MB      16             [big]
                            17.imports.js     681 kB      17             [big]
                            18.imports.js     141 kB      18
                            19.imports.js     141 kB      19
                            20.imports.js     141 kB      20
                            21.imports.js     141 kB      21
                            22.imports.js     141 kB      22
                            23.imports.js     141 kB      23  [emitted]
                            24.imports.js     141 kB      24
                               imports.js     421 kB      25  [emitted]  [big]  main
    23.d71b0f87e6ade357067e.hot-update.js  850 bytes      23  [emitted]
                             1.imports.js    1.49 MB       1             [big]
                         0.imports.js.map    2.05 MB       0
                         1.imports.js.map    1.47 MB       1
                         2.imports.js.map    1.47 MB       2
                         3.imports.js.map    1.47 MB       3
                         4.imports.js.map    1.47 MB       4
                         5.imports.js.map    1.47 MB       5
                         6.imports.js.map    1.47 MB       6
                         7.imports.js.map    1.47 MB       7
                         8.imports.js.map    1.46 MB       8
                         9.imports.js.map    1.47 MB       9
                        10.imports.js.map    1.47 MB      10
                        11.imports.js.map    1.47 MB      11
                        12.imports.js.map    1.47 MB      12
                        13.imports.js.map    1.47 MB      13
                        14.imports.js.map    1.47 MB      14
                        15.imports.js.map    1.47 MB      15
                        16.imports.js.map    1.28 MB      16
                        17.imports.js.map     812 kB      17
                        18.imports.js.map     169 kB      18
                        19.imports.js.map     169 kB      19
                        20.imports.js.map     169 kB      20
                        21.imports.js.map     169 kB      21
                        22.imports.js.map     169 kB      22
                        24.imports.js.map     169 kB      24
                        23.imports.js.map     168 kB      23  [emitted]
23.d71b0f87e6ade357067e.hot-update.js.map  481 bytes      23  [emitted]
                           imports.js.map     502 kB      25  [emitted]         main
webpack: Compiled successfully.

@gaearon
Copy link
Contributor

gaearon commented May 16, 2017

From a brief chat with @sokra, my impression that #4636 (comment) does not actually demonstrate a perf regression, but rather the difference in how require.ensure() and import() work. (Correct me if I’m wrong.)

With require.ensure(), anything you require() in the block becomes a part of one chunk. So this example puts all the “dynamic” modules (which we have a lot if we use dynamic expressions) in one JS chunk. This seems worse for the app performance, as, for example, if you load locale files based on current language with a statement like this, you’re going to load all of them in one file. But it’s faster to process at build time. This behavior seems optimal if bundles contain a lot of common code, and so it's not a big deal to combine them together.

With import(), however, every single possible match gets its own chunk. This seems better for the app performance, and it maps more intuitively to how you’d expect it to work, but is worse for the build performance because of all those chunks. This behavior seems optimal if bundles are different enough and contain little common code (of if you rarely need to switch them).

So require.ensure() default behavior seems to work better for things like component trees, but import() default behavior seems to work better for things like locales.

Frankly, I don’t know what the intended solution is here, but I’d love to hear more from Webpack folks on how this feature should be used, and what the intended performance is.

Apparently the import() default behavior is different from require.ensure() default behavior, but you will be able to customize it with hints to the import() call: #4807 (comment).

@ingro
Copy link

ingro commented May 17, 2017

Thanks for the insight @gaearon , I thought that using import or require.ensure was just a matter of syntax, while it's clear now that they behave very differently. I'll wait for the hints to be implemented for the import syntax, untile then I need to stick to the old require.ensure.

@AustinHunt
Copy link
Author

AustinHunt commented May 17, 2017

@gaearon, this makes quite a bit of sense. My though for a solution is to have an option that allows us to disable dynamic import and require.ensure in development. The main reason I use import and require.ensure are to chunk in production.

We are willing to have a longer build time for pushing to production, but it is debilitating for development.

@webpack-bot
Copy link
Contributor

This issue had no activity for at least half a year.

It's subject to automatic issue closing if there is no activity in the next 15 days.

@webpack-bot
Copy link
Contributor

Issue was closed because of inactivity.

If you think this is still a valid issue, please file a new issue with additional information.

@pratikt-cuelogic
Copy link

is this applicable for latest versions as well ?
I am using webpack 3.5.x and recently modified implementation for Dynamic Import using react-loadable and feel that build time has been increased.

Please suggest better solution to do so

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants