技術探し

JavaScriptを中心に記事を書いていきます :;(∩´﹏`∩);:

webpack@5の主な変更点まとめ

予定では、明日の10日にwebpackのメジャーバージョンであるv5がリリースされますが、まだエコシステムが安定していない可能性があるため、注意してアップグレードを行ってください。

github.com

change log:

github.com

移行ガイド:

webpack.js.org

追加機能

Persistent Caching

このバージョンからは今までメモリ上でしか行ってなかったファイルシステムによるキャッシュが導入されます。以下のように設定することにより、大幅な速度改善が見込めます。

module.exports = {
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    }
  }
};

なし ↓

asset main.js 36.3 KiB [emitted] [minimized] (name: main)
orphan modules 584 KiB [orphan] 554 modules
cacheable modules 117 KiB
  ./src/index.js + 103 modules 117 KiB [built] [code generated]
  ./src/foo.js 21 bytes [built] [code generated]
webpack 5.0.0-rc.2 compiled successfully in 1836 ms

あり↓

asset main.js 36.3 KiB [compared for emit] [minimized] (name: main)
cached modules 700 KiB [cached] 556 modules
webpack 5.0.0-rc.2 compiled successfully in 429 ms

詳しくは以下の記事を参照にしてください。

blog.hiroppy.me

Module Federation

リポジトリ間(バンドル間)を跨ぐときにライブラリなどの重複しているコードを以下のように効率よく扱いバンドルサイズを下げる仕組みです。この機能はお互いのwebpackと連携を取り合う必要があるため互いにwebpack@5である必要があります。

https://cdn-ak.f.st-hatena.com/images/fotolife/a/about_hiroppy/20200507/20200507073925.png

詳しくは以下の記事を参考にしてください。

blog.hiroppy.me

assetModules typeの追加

今まで画像などを読み込むときに、file-loaderやurl-loader, raw-loaderなどを使っていましたがそれがネイティブサポートされました。

module.exports = {
  output: {
    assetModuleFilename: 'images/[hash][ext]',
  },
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        type: 'asset/resource'
      }
    ]
  }
};

詳しくは以下の記事を参考にしてください。

blog.hiroppy.me

チャンク名がIDへ変更

今まで以下のようにwebpackChunkNameと書かなければ読めないファイル名となっていましたが、人が読める形となります。それに伴い、開発中でのwebpackChunkNameの指定をする必要がなくなることが期待されます。

(async () => {
  await import(/* webpackChunkName: "foo" */ './foo');
})();

名前をつけたときの出力

asset main.js 2.79 KiB [emitted] [minimized] (name: main)
asset foo.js 114 bytes [emitted] [minimized] (name: foo)
runtime modules 7.23 KiB 10 modules
cacheable modules 217 bytes
  ./src/index.js 190 bytes [built] [code generated]
  ./src/foo.js 27 bytes [built] [code generated]
webpack 5.0.0-rc.4 compiled successfully in 274 ms

v5のデフォルトでは以下のようにdeterministicという設定の新しいアルゴリズムが追加され、モジュール/チャンクの名前に3~4桁の数値IDが付与されるようになります。これにより、ハッシュ化されたモジュールIDによるgzipでのパフォーマンス低下は修正されました。

asset main.js 2.79 KiB [emitted] [minimized] (name: main)
asset 717.js 114 bytes [emitted] [minimized]
runtime modules 7.23 KiB 10 modules
cacheable modules 186 bytes
  ./src/index.js 159 bytes [built] [code generated]
  ./src/foo.js 27 bytes [built] [code generated]
webpack 5.0.0-rc.4 compiled successfully in 283 ms

ファイル名を自動的に付与したい場合

module.exports = {
  optimization: {
    chunkIds: 'named'
  }
};
asset main.js 2.8 KiB [emitted] [minimized] (name: main)
asset src_foo_js.js 123 bytes [emitted] [minimized]
runtime modules 7.23 KiB 10 modules
cacheable modules 186 bytes
  ./src/index.js 159 bytes [built] [code generated]
  ./src/foo.js 27 bytes [built] [code generated]
webpack 5.0.0-rc.4 compiled successfully in 256 ms

optimization.chunkIds に named を追加すればファイル名が確定しますが本番環境では表示されていいものなのかを検討してください。また、optimization.splitChunks.nameはなくなったのでこちらに移行してください。

import.metaのサポート

// ./src/index.js
console.log(import.meta.url);
console.log(import.meta.webpack);
// ./dist/main.js 
// 生成されたファイルは固定値として入り、import.meta.url, webpackは存在しなくなる
console.log("file:///Users/hiroppy/webpack/src/index.js");
console.log(5); 

また、HMR時に今までは以下のように書いていましたが、これからはimport.meta.webpackHotを使うことが可能です。これを使うことにより、Node.jsのmoduleへの依存を減らし、ESMに沿うような書き方に変わります。

// <= 4
if (module.hot) {
  module.hot.accept();
}

// >= 5
if(import.meta.webpackHot) {
  import.meta.webpackHot.accept();
}

// or
import.meta.webpackHot?.accept(); 

data, file, http(s)のプロトコルのサポート

import x from 'data:text/javascript,export default 42';
console.log(x); // 42

import y from 'file:///Users/hiroppy/webpack/src/index.js';

また、フラグメント(#)もサポートされました。

const eIndexOf = require('es5-ext/array/\0#/e-index-of#fragment');

http(s)プロトコルは、まだ完全にサポートされていないため以下の設定が必要です。

const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.experiments.schemes.HttpUriPlugin(),
    new webpack.experiments.schemes.HttpsUriPlugin()
  ]
};

// index.js
import codeOfConduct from 'https://raw.githubusercontent.com/webpack/webpack/master/CODE_OF_CONDUCT.md';
console.log(codeOfConduct);

Native Workerのサポート

new Worker(new URL('...', import.meta.url))がWebWorkerを作るようにサポートされました。これはSharedWorkerも同様です。

const fooWorker = new SharedWorker(new URL("./foo-worker.js", import.meta.url), {
  name: 'foo'
});

publicPathの自動化

新しくデフォルト値としてautoが追加され、document.currentScript, document.getElementsByTagName('script'), self.location の中から自動的に決定されます。注意点として、IEではdocument.currentScriptがサポートされていないため、deferred か async のスクリプトには使用することができません。

module.exports = {
  output: {
    publicPath: 'auto'
  }
};

Tree Shakingの最適化

ネストされたモジュールの場合、今までは使われていないbは削除できませんでしたがv5からは追跡可能となりできるようになりました。

// inner.js
export const a = 1;
export const b = 2;

// module.js
import * as inner from "./inner";
export { inner }

// user.js
import * as module from "./module";
console.log(module.inner.a);

v4では、モジュールの関係性しか見ていませんでしたが、v5から入ったoptimization.innerGraphにより、内部モジュールへの最適化も行えるようになりました。

import { something } from "./something";

function usingSomething() {
  return something;
}

export function test() {
  return usingSomething();
}

以下のケースが対象です。

  • 関数宣言
  • クラス宣言
  • 変数宣言 及び export default

Optimization.sideEffectsでは、ソースコードから副作用のないモジュールの単純なケースを検出できるようになりました。クラスおよび関数宣言、簡単なinit式を使用した変数宣言、if、while、for、switch、export、import、簡単なフラグを使用した関数呼び出し 等です。

また、CJSもサポートされました。

  • module.exports = require('...')
  • module.exports.a.b.c = require('...').a.b.c
  • Object.defineProperty(module.exports, 'xxx', ...)
  • require('abc').xxx

このサポートは、ESM、CJS間でも動くので、今後どちらのモジュールシステムを使っているかを気にせずに最適化行えるようになります。

これは別の記事で詳細に説明するので予定です。

output.filename, output. chunkFilenameの関数化

output.filenameは今まで文字列しか受け取りませんでしたが、関数にすることが可能となったため更に柔軟な設定を表現することが可能となります。

module.exports = {
  output: {
    filename: ({ chunk }) => {
      if (chunk.name === 'main') return 'main.bundle.[contenthash].js';
      return 'foo.bundle.[contenthash].js'
    }
  }
};

externalsTypeの追加

externalsTypeにpromise, import, scriptが追加され、より柔軟に対応できるようになりました。

  • promise: varと同様だが、非同期モジュールとなる
  • import: import()を使い、非同期のネイティブESMモジュールを読み込む
  • script: <script>を使い、事前に定義されたグローバル変数を公開するスクリプトを読み込む
module.exports = {
  externalsType: 'promise'
};

targetの詳細化とbrowserslistのサポート

targetに対して、詳細な設定ができるようになりました。 配列を受け取るようになり、target: ['web', 'es2015'] 等の書き方が行えるようになりました。 また、browserslistがされたため、webの場合はtargetの設定は不要となります。

github.com

デフォルト値はtarget: 'browserslist'となり、フォールバック先は変わらずにwebとなります。

TypeScript型定義ファイルの提供

@types/webpackは不要になりました。

import { WebpackOptionsNormalized } from 'webpack';

const config: WebpackOptionsNormalized = {
  entry: 'index.js',
  output: {
    filename: 'bundle.js'
  }
};

splitChunksでのサイズ設定値の変更

今までは、JSのみのチャンクサイズでしたが、さらに詳細に指定できるようになりました。

module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        test: {
           name: 'test',
           minSize: {
             javascript: 100,
             webassembly: 100,
             style: 100,
          }
        }
      }
    }
  }
};

また、本番環境でのminSizeのデフォルト値は20kとなりました。

実験的段階

top-level-awaitのサポート

シンタックスはESMの仕様に沿いますが、まだstage-3なので実験的フェーズです。

github.com

// webpack.config.js
module.exports = {
  experiments: {
    topLevelAwait: true,
  }
};
const x = await import('file:///Users/hiroppy/Desktop/webpack-5/src/foo.js');

console.log(x);

scriptタグでのモジュールサポート

バンドル時に使われるIIFEが取り除かれ、<script type="module">経由で呼び出される形に出力されます。この場合、仕様に沿い厳格モードと遅延ロードが有効化されます。

module.exports = {
  experiments: {
    outputModule: true
  }
};

破壊的変更

最低要求バージョンがNode.js@10へ

webpack及びwebpackのコアにおけるエコシステムが要求するNode.jsのバージョンの最低値は10となります。

Node.jsのpolyfillの自動挿入が廃止

メンバー間でも賛否両論がありましたが、理由としては以下のような目的があります。

  • webpackはwebへ向かっている
  • polyfill自体が完全互換なものではない
  • メンテナンスコストの高さ

自分が経験した例としては、processやutilに依存しているNode.jsのコードをクライアントサイドで使う場合があり、v5に上げたら動かなくなる場合があります。

実際にwebpack4まで使っていたpolyfillは以下のリポジトリで管理されているので、これを参考にして各自で追加する必要があります。

github.com

これに伴い、node.*の中のネイティブモジュールがすべて廃止となります。 また、global, __filename, __dirnameはデフォルトでfalseの値となります。

module.exports = {
  node: {
    // Buffer: false, これは廃止
    global: false,
    __filename: false,
    __dirname: false,
  }
};

JSONでのnamed exportの禁止

ESMの仕様上、これは許可されていないためこれが行われているコードの場合警告が出るようになるため、以下のように変更する必要があります。

// 😵
import { version } from './package.json';

// 🙂
import package from './package.json';
const { version } = package;

loaderとuseの違いを厳格化

rules.loaderとrules.useで目的に合ってない使い方の設定の場合、エラーを吐くようになりました。 useはoptionsがない場合のみ使用可能(引数は受け入れ可)となり、optionsがある場合はloaderを使わなければなりません。

デフォルトランタイムが一部ES2015へ変更

webpackの生成するコードのデフォルトが一部es5からes2015となります。
これはあくまでもバンドルサイズを減らすことが目的なため、varからconstにはなったりせず、functionを() => {} となります。
もしIEをサポートしている場合は以下を追加する必要があります。

module.exports = {
  target: ['web', 'es5']
};

また、これは追加機能として用意されたbrowserslistを用いて回避することも可能です。

# browserslist
last 1 version