mizdra's blog

ぽよぐらみんぐ

polyfill を深堀りする

この記事ははてなエンジニア Advent Calendar 2020 5日目の記事です。4日目は id:syou6162 さんで、数字のバラ付きを考慮して意思決定する技術でした。

qiita.com

developer.hatenastaff.com

こんにちは、id:mizdra です。今年新卒としてはてなに入社し、WebアプリケーションエンジニアとしてGigaViewerというマンガビューワーを作っています。

最近のはてな社内では「tech-future」という、様々な技術を見つめ直すワーキンググループを運営しています。この会では、ある技術についての要点をまとめるだけでなく、その技術にまつわる歴史を紐解いて、今どういう状態にあるのかというのを整理します。そして全体を俯瞰して将来その技術がどういう方向に向かうのかを議論し、未来を予測する手がかりを作る、といったことをやっています。既に弊社のエンジニアから「tech-future」の取り組みの一部が公開されてますので、是非読んでみて下さい。

developer.hatenastaff.com

developer.hatenastaff.com

developer.hatenastaff.com

この記事では「(Web) Frontend-Ops」をテーマとして開催された回の内、polyfill に関する説明を切り出したものです。 Frontend-Opsを記事のテーマとしてまとめて紹介するのも良いのですが、polyfill だけでも色々と面白い話が出てきたので、特別に Frontend-Ops の中からピックアップして紹介しようと思います。

polyfillとは何か

polyfill とは、仕様で策定されている機能や Proposal *1 を、それがサポートされていない環境でエミュレートするためのライブラリです。これにより、モダンブラウザでしか実装されてない機能が古いブラウザ(IE11など)でも動かせるようになります。例えば String#startsWithのpolyfillは以下のようになります。

// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith#Polyfill より
if (!String.prototype.startsWith) {
    Object.defineProperty(String.prototype, 'startsWith', {
        value: function(search, rawPos) {
            var pos = rawPos > 0 ? rawPos|0 : 0;
            return this.substring(pos, pos + search.length) === search;
        }
    });
}

このように、その環境にあるAPIを組み合わせて、実装されていないAPIと互換性のあるものを作ることで、古いブラウザでも String#startsWithを呼び出せるようになります。

polyfill の由来

実は polyfill という言葉は元々あったものではなく、2009年に Remy Sharp 氏 (nodemonの作者) によって生み出された造語です。いくつもの(poly)テクニックを使用して足りない穴を埋める(fill)ことから「polyfill」と命名したとのことです。まさにピッタリの語ですね。元々 Introducing HTML5 という書籍を書くにあたって生み出された造語で、書籍を通じて次第に世の中に広まっていったとされています。当時の経緯については氏のブログで紹介されています。

remysharp.com

トランスパイラとの違い

仕様で策定されている機能やProposalをエミュレートする技術として「トランスパイラ」があります。説明だけ見るとpolyfillと似たように見えますが、ちゃんと違いがあります。

  • polyfillは関数、クラスなどのビルトインオブジェクトのエミュレートを行うもの
    • その環境にあるAPIを組み合わせて、実装されていないAPIと互換性のあるものを作る、というアプローチを取る
    • 全て実行時に行われる
    • ライブラリとして提供される
  • 一方トランスパイラは構文 (syntax) のエミュレートを行うもの
    • a ?? b(a !== undefined && a !== null) ? a : bに変換したり
    • 実行時にできないようなことはこっちでやるイメージ
    • ツールとして提供される

具体例を挙げると requestidlecallbackcore-js が polyfill、babel や TypeScript がトランスパイラに相当します。

shimとの違い

また同様に polyfill と同じように使われる言葉として「shim」というのがありますが、これもまたpolyfillとは違いがあります。

  • polyfill はエミュレーション対象の仕様と似たようなインターフェイスを持っていて、同じように扱えるもののことを指す
    • 新しいブラウザ向けにstr.startsWith('prefix')と書いたら、それが古いブラウザでも同じように動く、みたいな
    • str.startsWithByPolyfill('prefix')みたいにpolyfillのことを意識して書く必要がない
  • shimはエミュレーション対象の仕様と同じ機能を持っているものの、インターフェイスが異なるものを指す
    • 同じことを実現できるけど、古いブラウザ向けと新しいブラウザでは別々のAPIを使ってそれを実現する、とか
    • 最近だと lazysizes が shim の良い例
      • 画像の遅延ロードをしてくれるライブラリ
      • 新しいブラウザでは<img src"..." loading="lazy">が使えるけど、古いブラウザだとJSで動的にスクロール位置を判定して実現しないといけない
      • これを良い感じにラップして<img data-src"..." class="lazyload">で遅延ロードできるようにするというのが lazysizes の仕事
      • 古いブラウザでも遅延ロードを実現する都合上、<img src"..." loading="lazy">から微妙にAPIが変わってしまっている
      • こういうのを shim と呼んでいる

polyfill の限界

requestIdleCallbackの polyfill である requestidlecallback を例に挙げてみます。requestIdleCallbackは CPU の Idle time に渡されたコールバックを実行するための API ですが、この API は Safari や IE11 では実装されていません。そのため、そうしたブラウザでもrequestIdleCallbackを使ったアプリケーションを実行するには、requestidlecallback を読み込む必要があります。しかし、この polyfill は完全にrequestIdleCallbackの仕様に準拠している訳ではありません。というのも、polyfill というものは実行環境にあるAPIを組み合わせて、実装されていないAPIと互換性のあるものを作る、というアプローチを取っているため、実行環境のAPIで表現しきれないような挙動を実現することができないのです。requestIdleCallbackの例ではCPUが idle になっているかを知るAPIがSafariやIE11に存在しないため、requestIdleCallbackの仕様通りに動くpolyfillを作ることができません。そこでこうしたpolyfillでは多くの場合、可能な範囲で互換性を保ち、それっぽく動くよう設計されます。requestidlecallback ではsetTimeoutを用いてコールバックが非同期で実行されるという性質は維持しつつ、idle time でコールバックが実行されるという性質は諦めるような設計となっています。

このようにpolyfillが完全に仕様に準拠しているかは、そのpolyfillがエミュレーションしようとしている機能と、ターゲットとしている実行環境によって変わってきます。polyfillがあるからと言って必ずしも全てのブラウザで同じような挙動をする訳ではない、という点を抑えておきましょう。

polyfillの歴史

現在

今後

昔と比べてブラウザの自動アップデートが当たり前になり、仕様の標準化が進んでブラウザの差異は解消されつつありますが、全てのユーザに最新のバージョンのブラウザの利用を強制するのが困難なこと、またChrome/Safari/Firefoxのようにブラウザが複数存在し、それぞれの実装に差異があることを考えると、今後もcore-jsを使っていくことになるでしょう。以前と変わらず、必要に応じてcore-jsでサポートされていない部分のpolyfillを入れていくようにしましょう。

おまけ: バンドルサイズの削減

近年 Web Vitals などの文脈で、Webでもパフォーマンスが重視されつつありますが、この話は polyfill にも関わってきます。というのも、polyfill はバンドルに含めてユーザに配信するため、polyfill を導入することでバンドルサイズが増えてしまうからです。IE11でしか使われないpolyfillがChromeやFirefoxなどでもダウンロードされてしまう、といったように、本来polyfillを必要としていないブラウザで、余計なオーバヘッドが掛かってしまう恐れがあります。

そうした問題を解決するため、次のような対策が取られることがあります。

  • browserlist+useBuiltInsで必要なpolyfillのみがbundleされるように
  • <script type="module"> / <script nomodule>でエントリポイントを分け、nomodule側にのみpolyfillを仕込む
    • これでIE11だけでpolyfillがfetchされるようになる
    • <script type="module">をサポートするブラウザの中で更に分岐させてpolyfillを出し分ける、といったことまではできない
  • polyfill.io
    • UAを見てそのブラウザに必要なpolyfillだけを返してくれる

おまけ: Proposalにおけるpolyfill

  • まだ仕様化されていない機能をお試しするためにpolyfillが用意される場合もある

おわりに

polyfill の概要の説明から始まり、 閑話休題も挟みつつ、polyfill の今と昔、それから未来について見てきました。polyfill という小さなトピックを取り上げましたが、色々な話が出てきて面白かったですね。そのうち Hatena Developper Blog でも Frontend-Ops をテーマとした記事が色々と出てくると思うので、良かったらそちらもご覧になって下さい。

そうそう、面白い話というと近々弊社のイベントで登壇して、チームにおけるフロントエンドの属人化を頑張って解消していく話をする予定です。近々告知が出る予定ですので、良かったらそちらもご覧になって下さい。 => 告知でました! (2020/12/12 追記)

developer.hatenastaff.com

明日は id:stefafafan さんです。

qiita.com

*1:標準化される前の提案段階の機能のこと。代表的なものとして、ECMAScript の top-level await などがあります: https://github.com/tc39/proposal-top-level-await

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.