Make it to make it

いろいろ作ってアウトプットするブログ

Webpackをゼロから学ぶ(その2)

Tree shaking

使用していないdead codeを読み込むことはファイルサイズの増大につながり、適切ではないであろう。 Webpackにおいて、例えばライブラリやファンクションを読み込んでいるのに一切使っていない場合は、

  • デフォルトのproductionモードでは、distファイル側でそのライブラリが読み込まれることはない。
  • 一方でdevelopmentモードでは、distファイルにもライブラリなどが読み込まれる。

Side effects False

Tree shakingが効いているとしても、読み込まれているモジュールのexportファンクション外のスコープに不要な記述が存在する場合、importしているだけで実行されてしまう。

export default function () {
  console.log('hoge')
}

console.log('fuga')
import my_awesome_module from 'my_awesome_module'

// 実行しなくてもconsole.log('fuga')が反映されてしまう

そこで package.json"sideEffects", false の記述を追加することで、この副作用を防げる。

開発とプロダクションで設定ファイルを分ける

devとprodでは設定が異なってくるが、次のようにファイルを分けることで対応できる。

  • webpack.common.js : ここで基本的に開発を進めて、ファイルが大きくなったら分けて対応する。
  • webpack.dev.js : development
  • webpack.prod.js : production

ファイル分割時に必要なパッケージ webpack-merge

webpack-merge を利用して、次のようにcommonファイルを読み込んで使用することができる。

webpack.dev.js

const merge = require('webpack-merge')
const common = require('./webpack.common.js')

module.exports = merge(common, {
  mode: 'development',
  devtool: 'inline-source-map'
})
# 実行時
yarn run -s webpack --config webpack.dev.js

webpack.prod.js

const merge = require('webpack-merge')
const common = require('./webpack.common.js')

module.exports = merge(common, {
  mode: 'production',
  devtool: 'source-map'
})
# 実行時
yarn run -s webpack --config webpack.prod.js

cssを個別ファイルで用意したいときに必要なパッケージ mini-css-extract-plugin

デフォルトではcss/jsともにjsファイルに格納されてしまうので、こちらのモジュールでの作業が別途必要になる。 ついでに各アセットの最適化を行ったプラグインを追加した設定ファイルが以下の通り。

webpack.config.js

const path = require('path')
const Webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  mode: 'production',
  devtool: 'source-map',
  devServer: {
    contentBase: './dist',
    port: 1234,
    hot: true
  },
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/dist/'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      },
      {
        test: /\.(png|jpg|jpeg|svg|gif)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              publicPath: '/dist/img/',
              outputPath: 'img/',
              name: '[name].[ext]'
            }
          }
        ]
      }
    ]
  },
  optimization: {
    minimizer: [
      // new UglifyJsPlugin(),
      new OptimizeCssAssetsPlugin()
    ]
  },
  plugins: [
    new Webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      title: 'html webpack plugin',
      filename: 'index.html',
      template: 'index.html'
    }),
    new MiniCssExtractPlugin({
      filename: 'css/[name].css'
    }),
    new CleanWebpackPlugin()
  ]
}

なぜかUglifyJsPlugin由来のエラーが出てしまうが、ここはまたTODOということでで今度解決する。

CodeSplittingによる共通管理

jQueryなどのライブラリを一つひとつのファイルに全てバンドルしてしまうと、その容量は増大しすぎてしまう。 ためしにエントリーポイントファイルに読み込みしている2つのファイルで、それぞれでjQueryを読み込んでみる。

first.js

import $ from 'jquery'

const $msg1st = 'hoge'
$('body').append($msg1st)

console.log('First JS! Yeah!!')

second.js

import $ from 'jquery'

const $msg2nd = 'fuga'
$('body').append($msg2nd)

console.log('Second JS!')

それぞれのファイルがともにjQueryを含んでいるため、容量が600KBをオーバーしてしまう。 そこでCodeSplittingを有効化して、共通のファイル(jQuery)は外出しすることで対応を行う。

webpack.config.js

const path = require('path')
const Webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
// const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  mode: 'production',
  devtool: 'source-map',
  devServer: {
    contentBase: './dist',
    port: 1234,
    hot: true
  },
  entry: {
    firstBundle: './src/js/first.js',
    secondBundle: './src/js/second.js'
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/dist/'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      },
      {
        test: /\.(png|jpg|jpeg|svg|gif)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              publicPath: '/dist/img/',
              outputPath: 'img/',
              name: '[name].[ext]'
            }
          }
        ]
      }
    ]
  },
  optimization: {
    splitChunks: {
      chunks: 'all'
    },
    minimizer: [
      // new UglifyJsPlugin(),
      new OptimizeCssAssetsPlugin()
    ]
  },
  plugins: [
    new Webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      title: 'html webpack plugin',
      filename: 'index.html',
      template: 'index.html'
    }),
    new MiniCssExtractPlugin({
      filename: 'css/[name].css'
    }),
    new CleanWebpackPlugin()
  ]
}

Lazy loading

importはlazyload的にも読み込むことができる。必要なときだけファイルを読み込むというものだ。

import '../css/main.css'
import $ from 'jquery'

$('#login_btn').click(function () {
  import('../js/first.js').then(function (module) {
    module.some_func()
  })
})

$('#register_btn').click(function () {
  import('../js/register.js').then(function (module) {
    module.default()
  })
})