Fork me on GitHub

JSerレポート #2: Node.jsコアモジュールとBundler(webpackなど)によるpolyfillのギャップ

Edit on GitHub 編集履歴を見る

このレポートは、現在進行形で機能追加や仕様変更が行われているNode.jsコアモジュールとブラウザ向けpolyfillにおける挙動の違い(ギャップ)が広がってきている問題について調べたものです。

ここでは https://nodejs.org/api/ に掲載されているうち assertのようにNode.jsにバンドルされているモジュールのことをNode.jsコアモジュールと呼びます。コアモジュールはNode.jsでの利用のみを想定しているため、Node.jsに依存した処理を多く含んでいます。そのため、コアモジュールのコードをコピーしてブラウザなどで動かすことは難しいです。

webpackbrowserifyなどの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ライブラリは次の場所で管理されています。

どちらも基本的に利用しているpolyfill自体はほとんど同じです。

機能のギャップ

このレポートの本題であるNode.jsコアモジュールとブラウザ向けpolyfillのギャップがあったものをまとめた表です。
ここでいうギャップというのは、次のようなケースを並べています。

  • Node.jsコアモジュールで追加されたAPIがpolyfillライブラリには存在しない
  • Node.jsコアモジュールとpolyfillライブラリで挙動が異なる
  • 利用されているpolyfillライブラリがDeprecatedになっている

これらの調査結果については次のリポジトリで管理しています。最新の状況もこのリポジトリに反映しています。
そのため次の表は古くなっている可能性があります。

注記: 依存しているpolyfillそのものはアップデートで解決されている場合があります。しかし、bundlerが古いバージョンを使っている場合があります。

Node.js Browser polyfill Issue Link
assert browserify/commonjs-assert Error code and Error message are different Issue, Article
assert.deepEqual does't support Map, Set, Iterator etc... Issue, Document, Release
require("assert").strict Docs, Release
assert.rejects() Release
assert.doesNotReject() Release
Compatible issue with assert.fail(), assert.ok(), and assert.ifError() No arguments behavior. Release
buffer feross/buffer ---
child_process --- ---
cluster --- ---
console Raynos/console-browserify ---
constants juliangruber/constants-browserify ---
crypto crypto-browserify/crypto-browserify ---
dgram --- ---
dns --- ---
domain bevry/domain-browser ---
events Gozala/events eventNames Issue
getMaxListeners Issue
prependListener Issue
prependOnceListener Issue
off Issue
fs --- ---
http jhiesey/stream-http ---
https substack/https-browserify ---
module --- ---
net --- ---
os CoderPuppy/os-browserify os.constants
path browserify/path-browserify path.posix Issue
path.parse(path) Issue
path.win32
path.format(pathObject)
process defunctzombie/node-process process.channel
process.platform Issue
process.execArgv Issue
process.cpuUsage([previousValue])
process.emitWarning(warning[, options])
punycode bestiejs/punycode.js ---
querystring mike-spainhower/querystring ---
readline --- ---
repl --- ---
stream browserify/stream-browserify ---
string_decoder rvagg/string_decoder TODO Repository
sys defunctzombie/node-util TODO
timers browserify/timers-browserify ---
tls --- ---
tty browserify/tty-browserify ---
url defunctzombie/node-url url.URL(WHATWG URL) Release, Document, Issue
url.format does't support WHATWG URL Release, Document
util defunctzombie/node-util util.callbackify(original)
util.inspect.custom
util.inspect.defaultOptions
util.promisify(original)
util.promisify.custom
util.inspect() options maxArrayLength, breakLength
util.isDeepStrictEqual
util.isDeepStrictEqual
vm browserify/vm-browserify vm.isContext(sandbox)
zlib devongovett/browserify-zlib zlib.bytesRead

実装状況

この調査リポジトリには簡単な機能テストも実装されています。

次にそれぞれでのテスト結果を示します。

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ライブラリ群として暗黙的に参照されている点です。

多くのコアモジュールにおいては、問題が表面化しない可能性もあります。
しかし、asserteventsurlはブラウザ向けとしてよく使われているにもかかわらず、差異が分かる程度にはあります。
また、ギャップの問題が解決できた場合にも、バージョンを指定できずに暗黙的な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の今後に影響する可能性は高いと思います。

jser/report バックナンバー

この記事へ修正リクエストをする
JSer.info Slackに参加する