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

Reduce (drastically) bundle size #547

Closed
3 tasks
thomasthiebaud opened this issue Apr 2, 2017 · 20 comments
Closed
3 tasks

Reduce (drastically) bundle size #547

thomasthiebaud opened this issue Apr 2, 2017 · 20 comments

Comments

@thomasthiebaud
Copy link

Summary

I really love this library, but the bundle size is to huge !!! Even I only want to use VictoryLine, I need to import the full victory, victory-chart and victory-core library + lodash.

Here are some things to do in order to reduce bundle size, for victory, victory-chart and victory-core. I can do a PR for it

  • : Only import used lodash module

Replace

import { xxx, yyy } from "lodash";

by

import xxx from "lodash/xxx";
import yyy from "lodash/yyy";

  • : Only import used victory-core module

Replace

import { PropTypes as CustomPropTypes, Helpers } from "victory-core";

by

import CustomPropTypes from "victory-core/lib/victory-util/prop-types";
import Helpers from "victory-core/lib/victory-util/helpers";
  • : Add index.js when relevant

For example, add an index.js file in victory-core/src/victory-transition so

import VictoryTransition from "victory-core/lib/victory-transition/victory-transition";

can by replaced by

import VictoryTransition from "victory-core/lib/victory-transition";

Details

I'm trying victory with this sample

import { VictoryLine } from 'victory-chart'
<VictoryLine
  data={[
    {month: "September", profit: 35000, loss: 2000},
    {month: "October", profit: 42000, loss: 8000},
    {month: "November", profit: 55000, loss: 5000}
  ]}
  x="month"
  y={(datum) => datum.profit - datum.loss}
/>

But the bundle is way to big !!! I used webpack-bundle-analyser to find why.

Here is the graph I have with the current code

screen shot 2017-04-02 at 14 18 49

As you can see, even if I am only using VictoryLine, the full victory-core, victory-chart and lodash are included in my bundle

So I tried to directly install victory-core and victory-chart and change my code sample to

import VictoryLine from 'victory-chart/lib/components/victory-line/victory-line'
...

Using this method I reduced the bundle size and only include VictoryLine from victory-chart in my bundle. Here is the new graph I get

screen shot 2017-04-02 at 14 17 26

Problem :

  • lodash is still fully included. You are using (line 1)
import { partialRight, without } from "lodash";

(which include the whole lodash library) but you only need

import partialRight from "lodash/partialRight";
import without from "lodash/without";
  • victory-core is still fully included. You are using (line 4-8)
import {
  PropTypes as CustomPropTypes, Helpers, VictoryTransition, VictoryLabel, addEvents,
  VictoryContainer, VictoryTheme, DefaultTransitions, Curve, VictoryClipContainer,
  Data, Domain
} from "victory-core";

(which include the whole victory-core library) but you only need

import LineHelpers from "./helper-methods";
import CustomPropTypes from "victory-core/lib/victory-util/prop-types";
import Helpers from "victory-core/lib/victory-util/helpers";
import VictoryTransition from "victory-core/lib/victory-transition/victory-transition";
import VictoryLabel from "victory-core/lib/victory-label/victory-label";
import addEvents from "victory-core/lib/victory-util/add-events";
import VictoryContainer from "victory-core/lib/victory-container/victory-container";
import VictoryTheme from "victory-core/lib/victory-theme/victory-theme";
import DefaultTransitions from "victory-core/lib/victory-util/default-transitions";
import Curve from "victory-core/lib/victory-primitives/curve";
import VictoryClipContainer from "victory-core/lib/victory-clip-container/victory-clip-container";
import Data from "victory-core/lib/victory-util/data";
import Domain from "victory-core/lib/victory-util/domain";
@ryan-roemer
Copy link
Member

ryan-roemer commented Apr 2, 2017

Hi @thomasthiebaud ,

First, you'll want to use the lodash-webpack-plugin straight up like we do: https://github.com/FormidableLabs/builder-victory-component/blob/494324f7fba9a7ed810337720ad39c8fd4acfe8d/config/webpack/webpack.config.js#L69-L75

Second, tree-shaking from a direct dep on victory-chart npm module: import { VictoryLine } from "victory-chart/src" instead of raw victory. Use webpack 2 with tree shaking configured (we wrote a post about that if you need help there).

Finally, we are still working a ticket to provide raw es6 sources everywhere to facilitate tree-sharking / es6-ness of the entire dependency tree.

In any case, the first two steps you can do now and should dramatically reduce your bundle!

@thomasthiebaud
Copy link
Author

Perfect, thank you very much

@thomasthiebaud
Copy link
Author

@ryan-roemer I did the first two steps, but the size remains the same

screen shot 2017-04-02 at 16 31 42

@ryan-roemer
Copy link
Member

Can we see your webpack config and .babelrc?

@ryan-roemer
Copy link
Member

Ah, it may be from lodash in victory-core needing the lodash-webpack-plugin too. Without that ticket implemented, may need a little more exotic config which is pin a dep of victory-core in your package.json that matches victory-chart's nested one, then edit your webpack config with:

resolve: {
  alias: {
    // Force use of es6 sources instead of es5 lib.
    "victory-core": require.resolve("victory-core/src")
  }
}

I haven't tested this, and it may need a little more finnagling...

@thomasthiebaud
Copy link
Author

Yes sure, here are my configuration. I tried the alias, but I have some error (missing loader on a .js file, I don't know why yet)

webpack.config.js

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const path = require('path')
const webpack = require('webpack')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const project = require('./project.config')

const __DEV__ = project.globals.__DEV__
const __PROD__ = project.globals.__PROD__
const __TEST__ = project.globals.__TEST__

const APP_ENTRIES = [project.paths.client('index.js')]

if (__DEV__) {
  APP_ENTRIES.unshift(
    'react-hot-loader/patch',
    `webpack-dev-server/client?http://${project.server_host}:${project.server_port}`,
    'webpack/hot/only-dev-server'
  )
}

const config = {
  devtool: project.compiler_devtool,

  externals: {
    leaflet: 'L',
  },

  entry: {
    app: APP_ENTRIES,
    vendor: project.compiler_vendors,
  },

  output: {
    filename: `[name].[${project.compiler_hash_type}].js`,
    path: project.paths.dist(),
    publicPath: project.compiler_public_path,
  },

  resolve: {
    modules: [
      project.paths.client(),
      'node_modules',
    ],
    alias: {
      config: path.resolve(project.paths.client(), 'config', project.flavor),
    },
  },

  module: {
    rules: [{
      test: /\.js$/,
      loader: 'babel-loader',
      exclude: /node_modules/,
    }, {
      test: /\.css$/,
      use: [{
        loader: 'style-loader',
      }, {
        loader: 'css-loader',
        options: {
          minimize: true,
        },
      }],
    }, {
      test: /\.scss$/,
      use: [{
        loader: 'style-loader',
      }, {
        loader: 'css-loader',
        options: {
          localIdentName: '[name]__[local]___[hash:base64:5]',
          modules: true,
          importLoaders: 1,
          minimize: true,
        },
      }, {
        loader: 'postcss-loader',
        options: {
          plugins: () => [
            require('autoprefixer'),
          ],
        },
      }, {
        loader: 'sass-loader',
        options: {
          includePaths: [].concat(project.paths.client('styles')),
          data: '@import "base.scss";',
        },
      }],
    }, {
      test: /\.(png|jpg)$/,
      loader: 'url-loader',
      options: {
        limit: 8192,
      },
    }],
  },

  plugins: [
    new BundleAnalyzerPlugin(),
    new LodashModuleReplacementPlugin({
      'currying': true,
      'flattening': true,
      'paths': true,
      'placeholders': true,
      'shorthands': true,
    }),
    new webpack.DefinePlugin(project.globals),
    new webpack.optimize.OccurrenceOrderPlugin(),
  ],

  node: {
    fs: 'empty',
    net: 'empty',
    tls: 'empty',
  },
}

if (__DEV__) {
  config.plugins.push(
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin(),
    new HtmlWebpackPlugin({
      template: project.paths.public('index.html'),
      hash: false,
      filename: 'index.html',
      inject: 'body',
    })
  )
} else if (__PROD__) {
  config.plugins.push(
    new HtmlWebpackPlugin({
      inject: true,
      template: project.paths.public('index.html'),
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeRedundantAttributes: true,
        useShortDoctype: true,
        removeEmptyAttributes: true,
        removeStyleLinkTypeAttributes: true,
        keepClosingSlash: true,
        minifyJS: true,
        minifyCSS: true,
        minifyURLs: true,
      },
    }),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        screw_ie8: true, // React doesn't support IE8
        warnings: false,
      },
      mangle: {
        screw_ie8: true,
      },
      output: {
        comments: false,
        screw_ie8: true,
      },
    }),
    new webpack.optimize.AggressiveMergingPlugin())
}

if (!__TEST__) {
  config.plugins.push(
    new webpack.optimize.CommonsChunkPlugin({
      names: [
        'vendor',
      ],
    })
  )
}

if (__TEST__) {
  config.module.rules.push({
    test: /\.spec\.js?$/,
    loader: 'babel-jest',
    exclude: /node_modules/,
  })
}

if (!__DEV__) {
  config.module.rules
    .filter(rule => String(rule.test).includes('css'))
    .forEach((rule) => {
      const first = rule.use[0]
      const rest = rule.use.slice(1)
      rule.use = ExtractTextPlugin.extract({
        fallback: first,
        use: rest,
      })
    })

  config.plugins.push(
    new ExtractTextPlugin({
      filename: '[name].[contenthash].css',
      allChunks: true,
    }))
}

module.exports = config

project.config.js

const path = require('path')
const pck = require('../package.json')

const environments = require('./env.config')

const config = {
  env: process.env.NODE_ENV || 'development',
  flavor: process.env.NODE_FLAVOR || 'local',

  path_base: path.resolve(__dirname, '..'),
  dir_client: 'src',
  dir_dist: 'dist',
  dir_public: 'public',

  server_host: 'localhost',
  server_port: process.env.PORT || 3000,

  compiler_devtool: 'eval',
  compiler_public_path: '/',
  compiler_hash_type: 'hash',
  compiler_fail_on_warning: false,
  compiler_stats: {
    chunks: false,
    chunkModules: false,
    colors: true,
  },
  compiler_vendors: [
    'normalize.css',
    'react',
    'react-dom',
    'react-hot-loader',
    'react-redux',
    'react-router-dom',
    'react-table',
    'redux',
    'redux-saga',
    'reselect',
  ],
}

function base (...args) {
  return path.resolve(...[config.path_base].concat(args))
}

config.paths = {
  base,
  client: base.bind(null, config.dir_client),
  dist: base.bind(null, config.dir_dist),
  public: base.bind(null, config.dir_public),
}

config.globals = {
  'process.env': {
    NODE_ENV: JSON.stringify(config.env),
  },
  NODE_ENV: config.env,
  __DEV__: config.env === 'development',
  __PROD__: config.env === 'production',
  __TEST__: config.env === 'test',
}

const overrides = environments[config.env]
if (overrides) {
  Object.assign(config, overrides(config))
}

module.exports = config

.babelrc

{
  "presets": [
    ["es2015", {"modules": false}],
    "stage-2",
    "react"
  ],
  "plugins": [
    "lodash",
    ["transform-runtime", {
      "polyfill": false,
      "regenerator": true
    }],
    ["import", { "libraryName": "antd", "style": "css" }]
  ],
  "env": {
    "test": {
      "plugins": [
        "system-import-transformer",
        "transform-es2015-modules-commonjs"
      ]
    }
  }
}

@ryan-roemer
Copy link
Member

Oh, yeah, you'll need to amend that exclude: /node_modules/ to include node_modules/victory-core now to get babel processed -- maybe switching to an include directive with victory-core and your sources?

@thomasthiebaud
Copy link
Author

I no longer have the error with

include: [
  /node_modules\/victory-core/,
  /src/,
],

But the bundle looks like the same

screen shot 2017-04-02 at 17 17 43

@ryan-roemer
Copy link
Member

ryan-roemer commented Apr 2, 2017 via email

@thomasthiebaud
Copy link
Author

No, it remains the same

@thomasthiebaud
Copy link
Author

But if I use

import VictoryLine from 'victory-chart/src/components/victory-line/victory-line'

I get (no more victory-chart but still victory-core and lodash)

screen shot 2017-04-02 at 18 05 47

@ryan-roemer
Copy link
Member

Let me pull a branch in victory-chart on the demo that maybe we can both hack on to see what's up...

Also, I'm seeing victory-core/lib in your analyzed bundle.

Final question -- can you check you actual bundle to make sure that the lodash stuff isn't excluded?

@thomasthiebaud
Copy link
Author

I tried both with victory-core/lib and victory-core/src but the results are exactly the same. I tried to search for some lodash functions into my bundle and I was able to find them.

Here are more details about my bundle (classical output from webpack)

Webpack compilation completed
Hash: 7fa8572dd2969c729b17
Version: webpack 2.3.2
Time: 50502ms
                                      Asset       Size  Chunks                    Chunk Names
                  5.a7679899076cb1763a7c.js    52.7 kB       5  [emitted]         
       b56ef938f034f4024bf903e4a84661ed.png    14.9 kB          [emitted]         
                  1.80e4f98b79ba11e76c53.js     204 kB       1  [emitted]         
                  2.323c7fee90c0a395d912.js     656 kB       2  [emitted]  [big]  
                  3.17b3f1603cc0af9add7b.js    80.8 kB       3  [emitted]         
                  4.06281da8615e4f9f093d.js    84.4 kB       4  [emitted]         
                  0.d8b3e5d2266158e620b0.js     357 kB       0  [emitted]  [big]  
                app.a301cf0ca86e3bd75d4e.js     231 kB       6  [emitted]         app
             vendor.3876ee80e186f2eb1c5f.js     269 kB       7  [emitted]  [big]  vendor
   app.3a197265f83f8e3c41bdcaba38cea54c.css     206 kB       6  [emitted]         app
vendor.659d5b29c7a69fcec90ce077c2814915.css    2.15 kB       7  [emitted]         vendor
                                 index.html  682 bytes          [emitted]         
Child html-webpack-plugin for "index.html":
         Asset    Size  Chunks  Chunk Names
    index.html  545 kB       0

The chunk 2 jumped from 200kb to 656kb after installing victory

@thomasthiebaud
Copy link
Author

I am already using the lodash plugin (but I have more presets)

@ryan-roemer
Copy link
Member

I'm working in here: https://github.com/FormidableLabs/victory-line-experiments

It doesn't quite build yet (need some babel hackery still), but perhaps we can isolate a solution here. Also, I'm tweaking my uglify settings to only do DCE but otherwise preserve prettified source and comments to make direct bundle analysis easier.

I may not have a chance to finish getting this building, but should be a good start and exercise for us with "just the problem" focus.

@ryan-roemer
Copy link
Member

I've also invited you as a contributor on that if you want to branch and hack more easily...

Thanks for rolling through this experiment with us! I'm not so much a React guy as a "build guy", and I've been meaning to get more involved in tuning of Victory for production projects, so this is all great! (assuming we figure everything out now 😉 )

@thomasthiebaud
Copy link
Author

Thanks, I will have a look tonight or tomorrow

@ryan-roemer
Copy link
Member

OK, https://github.com/FormidableLabs/victory-line-experiments now works as expected! The trick was to apply parsing to both victory-core and victory-chart to get lodash plugin + tree shaking / DCE working...

The hackery includes:

Babel parsing both victory-core and victory-chart:

// Need to resolve to the **directory** of `src`.
var resolveSrc = function (mod) {
  return path.join( path.dirname(require.resolve(mod + "/package.json")), "src");
};

// Need to babel process both `victory-core` and `victory-chart` and alias.
var victoryCoreSrc = resolveSrc("victory-core");
var victoryChartSrc = resolveSrc("victory-chart");

// ...
  module: {
    loaders: [
      {
        test: /\.js$/,
        include: [
          path.resolve("src"),
          victoryCoreSrc,
          victoryChartSrc
        ],
        loader: "babel-loader"
      }
    ]
  },

And aliasing both to src:

  resolve: {
    alias: {
      "victory-chart": victoryChartSrc,
      "victory-core": victoryCoreSrc
    }
  },

(victory-chart alias is just a nice to have to allow imports from victory-chart like normal).

Can you run the numbers here and see if everything looks like what you'd want?

@thomasthiebaud
Copy link
Author

Cool, I will have a look. I played on the develop branch, but I had the same problem

@ryan-roemer
Copy link
Member

I've updated https://github.com/FormidableLabs/victory-line-experiments#analysis---one-off-vs-using-index with a now observed bug (most likely in upstream webpack) whereby tree shaking does not completely work.

I've opened up #549 to address this.

@thomasthiebaud -- My experiment code should mostly take care of the lodash issues because the plugin should now work to remove root lodash, and most of the honing down. It just will miss lodash methods used by upstream components where webpack2 tree shaking is broken.

Hopefully there's enough here to give you a start on significantly honing down your own bundle (especially with new one-off-import.js example) with existing Victory modules while we unpack that now somewhat diagnosed hairier issue of: #549

boygirl added a commit that referenced this issue Jul 17, 2018
make sure downsample is working for plotting functions
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants