こんにちは、クレイの阪本です。
最近長らく住んでいた東京を離れ神戸市へ移住しました。
蒙古タンメン中本や二郎など好みのラーメンに手が届きづらくなってしまったので新たな好物を探索中です。
本題に移りますが、DocBaseのシステムはRuby on Rails + ReactのSPA構成となっています。また、フロントエンドのビルドシステムはwebpacker@5.4を採用しています。
内部的にはwebpack v4が使われており、webpacker@6.xはまだbetaリリースの状態です(2021年6月時点)。webpacker@6.xではwebpack v5ベースへのマイグレーションが入るようです。
今回はwebpackベースのビルドシステムを構築する際に、DocBaseで利用したtipsをいくつかご紹介できればと思います。
Webpackerのコンフィグは部分的にejectして運用する
Webpackerはwebpackのコンフィグをwrapして生成してくれるのですが、公式ドキュメントに従いコンフィグの変更を行った場合に、どのような動作になるのか分かりづらくなる印象がありました。
Webpackerの公式ドキュメントでは、cssに関する設定のsplice
やmap
などがWebpacker内部のコードに依存していました。最終的に出力されるコンフィグを整形するために、苦労された方も多いのではないでしょうか?
loaderへの変更をかける場合を一例としてあげてみます。
- 例1
// resolve-url-loader must be used before sass-loader
environment.loaders.get('sass').use.splice(-1, 0, {
loader: 'resolve-url-loader'
});
- 例2
// webpack/environment.js
const { environment } = require('@rails/webpacker')
// replace css-loader with typings-for-css-modules-loader
environment.loaders.get('moduleSass').use = environment.loaders.get('moduleSass').use.map((u) => {
if(u.loader == 'css-loader') {
return { ...u, loader: 'typings-for-css-modules-loader' };
} else {
return u;
}
});
コンフィグを変更したいだけなのに、やたら複雑なコードになってしまいました。ここは改善したいところです。
例えば、create-react-appでは素のcreate-react-appを実行した場合、各パッケージの設定ファイルは隠蔽されています。ですがパッケージの設定を細かくやりたい場合、npm run eject
を実行することで各パッケージの設定をwebpack.config.jsとして出力できます。
これに近いことをWebpackerで行えないかを検討してみました。
webpackコンフィグを深い階層まで出力してみる
まずWebpackerのコンフィグが、webpackのコンフィグとしてどのように設定されているのかを出力することにしました。
@rails/webpacker
から得られるwebpackのコンフィグをprintするのに、node.js付属のinspect
を組み合わせて、深い階層まで出力できるようにしました。
※ 普段はノイズになるのでコメントアウトしています
- environment.js
const { environment } = require('@rails/webpacker')
const inspect = (obj) => console.log(require('util').inspect(obj, false, null))
// loaderの設定をすべて出力
inspect(environment.loaders.values)
// webpackのコンフィグをすべて出力
inspect(environment.toWebpackConfig())
プロジェクト独自のコンフィグを作成
sass/cssのコンフィグを前述の手段で出力された内容をベースに、改変したい箇所だけ変更します。これをプロジェクトのコンフィグとして取り出して配置します。
config/webpack/loaders/css.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { config } = require('@rails/webpacker')
const { resolve } = require('path')
const sass = {
test: /\.(scss|sass)$/i,
sideEffects: true,
use: [
(
config.extract_css
? MiniCssExtractPlugin.loader
: { loader: 'style-loader' }
),
{
loader: 'css-loader',
options: {
sourceMap: true,
importLoaders: 2,
modules: false
}
},
{
loader: 'postcss-loader',
options: {
config: { path: resolve() },
sourceMap: true
}
},
{
loader: 'resolve-url-loader' // 独自設定として追加
},
{
loader: 'sass-loader',
options: {
sourceMap: true,
implementation: require('sass'),
sassOptions: {
includePaths: config.additional_paths
}
}
}
]
}
const css = {
test: /\.(css)$/i,
sideEffects: true,
use: sass.use.slice(0, -1) // spliceは単一ファイル内のコンフィグから取り出すので視認性は確保できる
}
module.exports = {
css,
sass
}
- environment.js
const { css, sass } = require('./loaders/sass')
// デフォルトのコンフィグは削除
environment.loaders.delete('css')
environment.loaders.delete('sass')
environment.loaders.delete('moduleCss') // 使っていない不要なloader
environment.loaders.delete('moduleSass') // 同上
// 独自に改変したコンフィグを挿入
environment.loaders.append('css', css)
environment.loaders.append('sass', sass)
module.exports = environment
Webpackerはloaderごとにラベルが割り当てられているので、一度デフォルトのコンフィグをすっぱり削除します。その後にプロジェクト独自のコンフィグを追加するようにしました。
これにより内部的なコンフィグが可視化でき、他のメンバーが見ても理解しやすい設定になったのではないかと思います。
このやり方はstorybookやvue-cliなど、他のWebpackをwrapしたビルドシステムにも使えるかもしれません。
Webpackerのバージョンアップに追従するかどうか
Webpackerのバージョンは開発中にも継続してアップデートを重ねましたが、sass/cssのloaderの変更があった場合に、変更したファイルも追従するかは都度判断になります。
バージョン毎の変更も、create-react-appにおけるeject
レベルの大きなものはなく、特に困るケースには当たりませんでした。
このように部分的にeject
を行うほうが、DocBaseの開発スタイルにフィットしていたように思います。
開発環境のビルドパフォーマンスは定期的に見直す
アプリケーションが大きくなるにつれ、ビルド時間は徐々に低速になっていくものです。
DocBaseではビルドパフォーマンスを改善するため、以下のことをおこないました。
assetsのコンパイル結果をstatsに出力しない
Webpackerではwebpack-dev-serverの起動時・再コンパイル時にデフォルトでassetsのログが出力されます。
数十行にわたる「成功しました」というログを見せられてもノイズにしかならないので、これを開発環境ではoffにしています。
const environment = require('./environment')
environment.config.merge({
devServer: {
stats: {
assets: false
}
}
})
consoleには端的な情報だけを出力して、再コンパイル時の時間など表示が目立つようにしています。
これが普段より大きく跳ね上がったりしていることがあると、手の入れ時だと気づきやすくするためです。
ℹ 「wdm」: Compiling...
[hardsource:e3385075] Using 107 MB of disk space.
[hardsource:e3385075] Tracking node dependencies with: yarn.lock.
[hardsource:e3385075] Reading from cache e3385075...
ℹ 「wdm」: Hash: 7eb2c10b2d258ffb3272
Version: webpack 4.46.0
Time: 2889ms
Built at: 2021/05/25 16:37:41
ℹ 「wdm」: Compiled successfully.
No issues found.
ビルドを遅くしている犯人を探す
具体的にビルド時間の改善を行う際はspeed-measure-webpack-pluginが各loader/pluginのボトルネックを調べるのに有用でした。
DocBaseではrenovateによるパッケージアップデートを行っているのですが、CIを通過していても実は開発環境のビルド時間に影響を与えていたことがありました。
例えばeslint-webpack-pluginとは相性が良くなかったのか、古いバージョンの方が動作が安定しているなどがありました。
このような調査や改善は不定期に行うため、--analyze
オプションを付与してビルドした場合はspeed-measure-webpack-plugin
のレポートもコンソールに出力できるようにしています。
- config/webpack/development.js
if (process.argv.includes('--analyze')) {
const smp = new (require('speed-measure-webpack-plugin'))()
module.exports = smp.wrap(environment.toWebpackConfig())
} else {
module.exports = environment.toWebpackConfig()
}
ビルドが遅くなっていないかメンバーに聞く
webpack-dev-serverの起動やファイル保存に対し「遅い」と感じられているうちはまだ良いですが、慣れのせいか「開発サーバーはこういうもんだ」、「たぶん自分の環境だけ」と思ってしまうこともあるようです。
チームにはビルドシステムにほぼ触らないエンジニアもいるため、ビルド時間やlinterルールなど開発体験に不満はないかをヒアリングしてみるといいかもしれません。「実はなんとなく〇〇が不都合に感じていた」という意見が出る可能性があります。
「他のプロジェクトではDocBaseよりもっとビルド時間がかかっているので遅いとは思わなかった」などの意見もありました。
どこかのタイミングで、ビルドパフォーマンスが遅くなっていないかメンバーに聞いてみたり、「ビルドが遅いです」と言いやすくする環境作りも大切だと感じました。
node_modules以下をトランスパイルしない
webpacker@5.4ではデフォルトでnode_modules以下もトランスパイルされるようになっていますが、DocBaseではビルド速度を上げるため、node_modulesをloaderの適用範囲から外しています。
node_modulesをトランスパイルしないようにするには、environment.jsに以下の行を追加します。
const { environment } = require('@rails/webpacker')
// NOTE: nodeModulesはv6で本家から削除されるためwebpackerのアップグレードの際にこの行を削除
// ref: https://github.com/rails/webpacker/blob/master/CHANGELOG.md#600---2021-tbd
environment.loaders.delete('nodeModules')
検証
node_modules以下をトランスパイルしなかった場合、どのぐらい処理時間を短縮できるかspead-measure-pluginを使って計測してみました(はじめ読み方がわからなかった)。
最下行のresolve-url-loaderを起点に下から上へloaderの処理が行われ、babel-loaderの処理が終了したのが52.041secから25.44secとなり、30sec近く改善したことが確認できます。
- before
-
after
また、modules with no loaders
についてはmodules count
の値が増加しています。つまりnode_modules以下パッケージのコードに対しては、トランスパイルが行われなかったことも合わせて確認できます。
注意点
ただしレガシーブラウザへの対応が必要なプロジェクトでは、この改善を取り入れるには注意が必要です。node_modulesをloaderの適用範囲から外すとbabel-loaderのpolyfillが適用されなくなるため、レガシーブラウザでランタイムエラーが発生する可能性があります。
DocBaseはIE11/LagacyEdgeがサポート対象外であり、polyfillを適用する必要がないので、node_modulesをまるっとloaderから削除しています。
レガシーブラウザに対応する場合は、Vue-CliのtranspileDependencies
のように、特定パッケージのみをトランスパイルの対象にすることを検討していただくと良いかと思います。
https://cli.vuejs.org/config/#transpiledependencies
ちなみにWebpacker@6.0.0では、デフォルトでnode_modules以下がトランスパイルされなくなります。
CaseSensitivePathsプラグインも削除
CaseSensitivePathsプラグインも特にプロジェクトには必要ないので削除していますが、こちらはせいぜい2sec程度の改善となっていました。
- environment.js
const { environment } = require('@rails/webpacker')
environment.plugins.delete('CaseSensitivePaths')
その他のビルドパフォーマンス改善
端的に開発環境のサーバを停止/起動した際に効果が一番出たのは、コンパイル結果をファイル化して次回起動時のキャッシュとするhard-source-webpack-pluginでした。
こちらはwebpack5でPersistent Cachingが使えるようになるのでアップグレードの際には置き換わる予定です。
その他デフォルトのwebpackerの設定からビルドに時間を取る割に効果が薄い設定を削除を行っています。
production環境のビルドパフォーマンス改善
webpackerのproductionビルドのデフォルトと突き合わせていき、不要なものを削除、差し替えたいものを置き換えしています。
gz/brotliへの圧縮処理をWebpakerで行わない
Cloudfrontへビルド結果をアップロードしているため、gz/brotliの生成はビルドでは行わないようにしています。これらファイルの生成と配信はCloudfrontが担当しています。参考
minify処理でesbuildのランタイムを利用する
DocBaseの本番環境では、とある事情で低負荷かつ高速にビルドを完了させる必要がありました。
調査すると、minifyの処理にデフォルトで使われるTerserWebpackPluginが大きくリソースを専有していることが判明し、代替え手段としてesbuild-loaderを検証することにしました。
esbuild-loaderはwebpackの世界にesbuildのランタイムを持ち込むことができるパッケージです。
js/tsのトランスパイルが主要な機能ですが、minify処理のみをesbuildに置き換えることも可能です。
未だv1を迎えておらずproduction readyではないとのことだったので、慎重に検証を重ねた上で導入を決めました。
TerserPluginとの性能差としてはさすがesbuildといったところで、14倍以上高速にminify処理が改善され、現在も特に問題なく動作しています。
- before
-
after
他、例外通知や--analyze
フラグを渡した際に、ビルドの検証モードに入るコンフィグをざっと並べてみました。DocBaseでは以下のようなコンフィグで運用しています。
- config/webpack/production.js
const environment = require('./environment')
const sentry = require('./plugins/sentry')
const s3upload = require('./plugins/s3upload')
const { ESBuildMinifyPlugin } = require('esbuild-loader')
environment.config.optimization.minimizer = [
new ESBuildMinifyPlugin({
target: 'es2015',
charset: 'utf8',
minifyWhitespace: true,
minifyIdentifiers: true,
minifySyntax: true,
jsxFactory: 'h',
loader: 'jsx'
})
]
// NOTE: CloudFrontで対応するのでビルド時にgz/br化しない
environment.plugins.delete('Compression')
environment.plugins.delete('Compression Brotli')
environment.plugins.append('s3upload', s3upload)
environment.plugins.append('SentryWebpackPlugin', sentry)
// sentryにsourcemapをアップロードしつつ公開は行わない
environment.config.merge({
devtool: 'hidden-source-map'
})
if (process.argv.includes('--analyze')) {
const smp = new (require('speed-measure-webpack-plugin'))()
module.exports = smp.wrap(environment.toWebpackConfig())
} else {
module.exports = environment.toWebpackConfig()
}
おわりに
バンドルサイズの削減などは他のサイトでも多く語られていたため、DocBaseで採用したtipsを紹介させていただきました。
もし今回の内容がお読みいただいた方のプロジェクトのお役に立てば幸いです。
宣伝
DocBase は社内・社外の垣根を超えて情報共有ができる情報共有サービスです。
細かい権限設定ができるので、オープンな情報もクローズドな情報も DocBase に集約できます。そして大きな企業さまでも全員で使えるよう、セキュリティには最も力を入れています。
チームを作成して30日間は無料ですべての機能が使えるので、ぜひチームを作成して試してみてください。
このエントリーに対するコメント
日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)
- トラックバック
「いいね!」で応援よろしくお願いします!