このレポートは、現在進行形で機能追加や仕様変更が行われているNode.jsコアモジュールとブラウザ向けpolyfillにおける挙動の違い(ギャップ)が広がってきている問題について調べたものです。
ここでは https://nodejs.org/api/ に掲載されているうち assert
のようにNode.jsにバンドルされているモジュールのことをNode.jsコアモジュールと呼びます。コアモジュールはNode.jsでの利用のみを想定しているため、Node.jsに依存した処理を多く含んでいます。そのため、コアモジュールのコードをコピーしてブラウザなどで動かすことは難しいです。
webpackやbrowserifyなどのbundlerは、コード中にあるコアモジュールを代替モジュールへとすり替えます。この代替モジュールはブラウザ向けpolyfillライブラリとよび、このpolyfillライブラリはブラウザで動くようにNode.jsコアモジュールと同等また空のダミー実装をしています。
Node.jsコアモジュールのpolyfillライブラリの例
webpackとbrowserifyは変換時に、コード中に現れるassert
モジュールをcommonjs-assertというpolyfillライブラリに自動的にすり替えます。
const assert = require("assert")
というコードはwebpackなどでbundleすると、次のように書いたのと同じようにモジュールの差し替えが行われます。
const assert = require("commonjs-assert")
webpackでは、このNode.jsコアモジュールへの差し替えをnodeオプションによって設定が可能です。
polyfill library
webpackとbrowserifyが利用するpolyfillライブラリは次の場所で管理されています。
- webpack:
- browserify:
どちらも基本的に利用しているpolyfill自体はほとんど同じです。
機能のギャップ
このレポートの本題であるNode.jsコアモジュールとブラウザ向けpolyfillのギャップがあったものをまとめた表です。
ここでいうギャップというのは、次のようなケースを並べています。
- Node.jsコアモジュールで追加されたAPIがpolyfillライブラリには存在しない
- Node.jsコアモジュールとpolyfillライブラリで挙動が異なる
- 利用されているpolyfillライブラリがDeprecatedになっている
これらの調査結果については次のリポジトリで管理しています。最新の状況もこのリポジトリに反映しています。
そのため次の表は古くなっている可能性があります。
注記: 依存しているpolyfillそのものはアップデートで解決されている場合があります。しかし、bundlerが古いバージョンを使っている場合があります。
実装状況
この調査リポジトリには簡単な機能テストも実装されています。
次にそれぞれでのテスト結果を示します。
Node v11.5.0
24コのテストをすべてパス(これがpolyfillの元なので当然ですが…)
24 passing (146ms)
Browserify 16.2.3
4/24のテストをパス。
gap-test
assert
1) Error#code
2) assert.deepEqual
3) assert.strict
4) assert.rejects
5) assert.doesNotReject
events
6) off
✓ eventNames
✓ getMaxListeners()
✓ prependListener()
✓ prependOnceListener()
os
7) constants
path
8) posix
9) win32
10) parse
11) format
process
12) platform
13) execArgv
14) cpuUsage()
15) emitWarning()
url
16) URL
util
17) inspect.defaultOptions
18) callbackify()
19) promisify()
vm
20) isContext
4 passing (293ms)
20 failing
webpack 4.82.2
すべてのテストが失敗しました。
Gapリスト通りのpolyfillが使われています。
gap-test
assert
1) Error#code
2) assert.deepEqual
3) assert.strict
4) assert.rejects
5) assert.doesNotReject
events
6) off
7) eventNames
8) getMaxListeners()
9) prependListener()
10) prependOnceListener()
os
11) constants
path
12) posix
13) win32
14) parse
15) format
process
16) platform
17) execArgv
18) cpuUsage()
19) emitWarning()
url
20) URL
util
21) inspect.defaultOptions
22) callbackify()
23) promisify()
vm
24) isContext
0 passing (134ms)
24 failing
Node.jsコアモジュールのpolyfillの今後
このレポートは、webpackやbrowserifyを使っているとあまり意識されないpolyfillライブラリに潜在的な問題があることを調べる目的で書きました。
この問題の難しさは各polyfillライブラリの管理者やバランスが異なるにもかかわらず、polyfillライブラリ群として暗黙的に参照されている点です。
多くのコアモジュールにおいては、問題が表面化しない可能性もあります。
しかし、assert
、events
、url
はブラウザ向けとしてよく使われているにもかかわらず、差異が分かる程度にはあります。
また、ギャップの問題が解決できた場合にも、バージョンを指定できずに暗黙的なpolyfillライブラリを差し替える仕組みは、互換性の問題が発生するかもしれません。
- Node.js Errors — Changes you need to know about – Node.js Collection – Medium
assert
結果のError#name
などが異なるため、Node.jsでは通るがブラウザ(polyfill)では失敗するテストができる- MapやSetなどES2015以降のビルトインオブジェクトに対応していない
- Node v7 で入った WHATWG URL 実装について | blog.jxck.io
- Node.jsがブラウザのWHATWG URLをサポートしたが、ブラウザ(polyfill)ではサポートされていない
webpackなどにIssueで同様の問題を報告していましたが、このIssueについては特に進捗はありませんでした。
最近(2018年12月21日)になってwebpack 5 alphaが公開されました。
webpack 5では自動的にNode.jsコアモジュールのpolyfillを自動的に入れないようにする変更が予定されています。
(2018年12月25日時点ではただの予定であるため、該当Issueにおいてフィードバックを求めています。)
In the early days, webpack's aim was to allow running most node.js modules in the browser, but the module landscape changed and many module uses are now written mainly for frontend purposes.
-- https://github.com/webpack/changelog-v5/blob/master/README.md#automatic-nodejs-polyfills-removed
変更理由としてこのように書かれているのように、webpackはNode.jsモジュールをブラウザ向けにpack(polyfill)する役割から、フロントエンド向けに書かれたモジュールをbundleする役割へ変わってきています。
今まではBuffer
などNode.jsのコアAPIに対応するモジュールを自動的にbundleすることで、Node.js向けに書かれたモジュールをブラウザでも動かせるようにしていました。
一方で、現在ではブラウザ向けに書かれた多くのモジュールがあるため、webpackが自動的にpolyfillを入れる必然性が少なくなってきています。
また、Buffer
のpolyfillなどはファイルサイズがほどほどに大きいため、パフォーマンス面においては自動的にpolyfillを行わないメリットもあります。(polyfillを行うかどうかは、webpack 4でもnodeオプションによって設定が可能です)
少しブラウザとは異なりますが、React NativeのBundlerもNode.jsコアモジュールのpolyfillを自動的に差し替えない仕組みとなっています。
このように、BundlerがNode.jsコアモジュールのpolyfillを暗黙的に入れるという挙動の状況は少し変わりつつあります。
これはwebpack 5の変更予定にも書かれていたように、Bundlerの目的の1つがNode.js向けに書かれたモジュールをブラウザ向けに変換することでした。
しかし、現在は多くのブラウザ向けに書かれたモジュールがあり、Bundlerはそれを効率的に扱うという目的に変わってきている点も関係しているのかもしれません。
調査を終えて
今回の調査で感じたのは、Node.jsのコアモジュールとブラウザ向けのPolyfillといった一種の互換レイヤーに対して関心を持っている人の絶対数が少ないという印象です。Node.jsもコアAPIとしてブラウザと同じWHATWG URL APIを実装するなどいったブラウザとの相互運用性に関する取り組みも行われています。
しかし、このNode.jsコアモジュールのpolyfillという互換レイヤーに関しては暗黙的に扱われていることが多く、その互換性に問題があることについてはあまり言及されていません。
W3C TAGのPolyfills and the evolution of the Webというドキュメントでpolyfillがどうあるべきかということについて書かれています。
このNode.jsコアモジュールのpolfyillの問題もNode.jsとpolyfillのライフサイクルの違いからきている面があると思います。
ブラウザの仕様における壊れたpolyfillはグローバルの挙動を書き換えるため問題となることがありましたが、幸いにもNode.jsのコアモジュールのpolyfillの多くはモジュールやBundlerという仕組みの上に作られたものです。
しかしながら、このNode.jsコアモジュールのpolfyillも一定数利用者がいるため互換性という問題からは切り離すことが難しいです。(polyfillの1つであるeventsモジュールは500万/weekダウンロードされています)
この問題に深く関係しているのはwebpackやbrowserifyなどのbundlerであるため、bundlerの動きがそのままNode.jsコアモジュールのpolfyillの今後に影響する可能性は高いと思います。