Search code examples
typescriptwebpacktree-shaking

Not able to tree shake lodash in a Webpack, TypeScript project


my goal is to tree shake lodash (amongst others) in my webpack.prod.js. Here are my configuration files. For completeness sake I will also include the webpack.dev.js, webpack.common.js, tsconfig.json and package.json:

webpack.common.js:

const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif|obj)$/,
                use: [
                    'file-loader'
                ]
            },
            {
                test: /\.glsl$/,
                loader: 'webpack-glsl-loader'
            },
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/
            },
            {
                test: /\.ts$/,
                enforce: 'pre',
                loader: 'tslint-loader',
                options: { failOnHint: true }
            }
        ]
    },
    resolve: {
        extensions: [".tsx", ".ts", ".js"]
    },
    entry: {
        app: './src/index.ts'
    },
    plugins: [
        new CleanWebpackPlugin(['dist']),
        new HtmlWebpackPlugin({
            title: 'Production'
        })
    ],
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
};

webpack.dev.js:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const JarvisPlugin = require("webpack-jarvis");
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif|obj)$/,
                use: [
                    'file-loader'
                ]
            },
            {
                test: /\.glsl$/,
                loader: 'webpack-glsl-loader'
            },
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/
            },
            {
                test: /\.ts$/,
                enforce: 'pre',
                loader: 'tslint-loader',
                options: { failOnHint: true }
            }
        ]
    },
    resolve: {
        extensions: [".tsx", ".ts", ".js"]
    },
    entry: {
        app: './src/index.ts'
    },
    devtool: 'inline-source-map',
    devServer: {
        contentBase: './dist'
    },
    plugins: [
        new HtmlWebpackPlugin({ title: 'urknall-core dev' }),
        new BundleAnalyzerPlugin({ analyzerPort: 8888 }),
        new JarvisPlugin({ port: 1337 }),
    ],
    output: {
        filename: '[name].bundle.js',
        publicPath: '/'
    }
};

webpack.prod.js:

const webpack = require('webpack');
const merge = require('webpack-merge');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const common = require('./webpack.common.js');

module.exports = merge(common, {
    devtool: 'source-map',
    plugins: [
        new UglifyJSPlugin({
            sourceMap: true,
            test: /\.js($|\?)/i,
            uglifyOptions: {
                compress: true
            }
        }),
        new webpack.LoaderOptionsPlugin({
            minimize: true,
            debug: false
        }),
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify('production')
        }),
    ]
});

tsconfig.json:

{
  "compilerOptions": {
    "outDir": "./dist/",
    "declaration": true,
    "declarationDir": "./types",
    "sourceMap": true,
    "noImplicitAny": true,
    "module": "commonjs",
    "target": "es6",
  }
}

package.json:

{
  "name": "bla",
  "version": "0.0.1",
  "description": "",
  "main": "index.ts",
  "types": "./dist/types/",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config webpack.prod.js",
    "start": "webpack-dev-server --open --config webpack.dev.js",
    "watch": "webpack --watch"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/gl-matrix": "^2.4.0",
    "@types/lodash": "^4.14.104",
    "@types/node": "^8.9.4",
    "@types/webgl2": "^0.0.2",
    "@types/webpack-dev-middleware": "^1.12.3",
    "clean-webpack-plugin": "^0.1.18",
    "file-loader": "^0.11.2",
    "html-webpack-plugin": "^2.30.1",
    "image-webpack-loader": "^3.6.0",
    "style-loader": "^0.18.2",
    "ts-loader": "^2.3.7",
    "ts-node": "^3.3.0",
    "tslint": "^5.9.1",
    "tslint-loader": "^3.5.3",
    "typescript": "^2.7.1",
    "uglifyjs-webpack-plugin": "^1.2.2",
    "webpack": "^3.11.0",
    "webpack-bundle-analyzer": "^2.11.0",
    "webpack-dev-middleware": "^1.12.0",
    "webpack-dev-server": "^2.11.2",
    "webpack-glsl-loader": "^1.0.1",
    "webpack-jarvis": "^0.3.0",
    "webpack-merge": "^4.1.2"
  },
  "dependencies": {
    "gl-matrix": "^2.4.0",
    "lodash-es": "^4.17.5"
  }
}

With this configuration the app.bundle.js results in a file of 507 KB and the app.bundle.js.map is 1.6 MB. If I do not uglify the build output I can see that the entire lodash library is being written into the file. I am also seeing some other functions from i.e. gl-matrix being written to the file, although I am not even using them.

Also I run a webpack-bundle-analyzer in development and the output does not differ from production. Only the budle file is ~1.4 MB instead of 1.6 MB because of minification.

enter image description here

I am loading lodash only twice in the entire project, like:

import { some, find, cloneDeep } from 'lodash';

No more lodash functions are ever used. Why does Webpack inflate my app.bundle.js?

Is there any problem with my configuration?


Solution

  • Given the existing export style of the standard lodash package you have two options for solving this:

    1. Move to a lodash package that uses exports in a way that will socket directly into your import pattern. As mentioned by @alex-rokabilis in a comment, one available package for this is lodash-es
    2. Update your imports to work the exports used by the standard lodash package. If you look at lodash's npm page you can see that they provide support for cherry picking specific method from paths.

      import some from 'lodash/fp/some';
      import find from 'lodash/fp/find';
      import cloneDeep from 'lodash/fp/cloneDeep';