この記事ははてなエンジニア Advent Calendar 2020 5日目の記事です。4日目は id:syou6162 さんで、数字のバラ付きを考慮して意思決定する技術でした。
こんにちは、id:mizdra です。今年新卒としてはてなに入社し、WebアプリケーションエンジニアとしてGigaViewerというマンガビューワーを作っています。
最近のはてな社内では「tech-future」という、様々な技術を見つめ直すワーキンググループを運営しています。この会では、ある技術についての要点をまとめるだけでなく、その技術にまつわる歴史を紐解いて整理し、その上で全体を俯瞰して将来その技術がどういう方向に向かうのかを議論し、未来を予測する手がかりを作る、といった挑戦的な取り組みをしています。既に弊社のエンジニアから「tech-future」の取り組みの一部が公開されてますので、是非読んでみて下さい。
この記事では「(Web) Frontend-Ops」をテーマとして開催された回の内、polyfill に関する説明を切り出したものです。 最初は Frontend-Ops 全体をテーマとしてまとめて記事にしようと考えたのですが、polyfill だけでも色々と面白い話が出てきたので、今回は polyfill をピックアップして紹介しようと思います。
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 という書籍を書くにあたって生み出された造語で、書籍を通じて次第に世の中に広まっていったとされています。当時の経緯については氏のブログで紹介されています。
トランスパイラとの違い
polyfill と同じように、仕様で策定されている機能やProposalをエミュレートする技術に「トランスパイラ」があります。一見すると polyfill と同じように見えますが、ちゃんと違いがあります。
- polyfillは関数、クラスなどのビルトインオブジェクトのエミュレートを行うもの
- その環境にあるAPIを組み合わせて、実装されていないAPIと互換性のあるものを作る、というアプローチを取る
- 全て実行時に行われる
- ライブラリとして提供される
- 一方トランスパイラは構文 (syntax) のエミュレートを行うもの
a ?? b
を(a !== undefined && a !== null) ? a : b
に変換したり- 実行時にできないようなことはこっちでやるイメージ
- ツールとして提供される
具体例を挙げると requestidlecallback や core-js が polyfill、babel や tsc (TypeScript のコンパイラ) がトランスパイラに相当します。
shimとの違い
また同様に polyfill と同じように使われる言葉として「shim」というのがありますが、これもまたpolyfillとは違いがあります *2。
- polyfill はエミュレーション対象の仕様と似たようなインターフェイスを持っていて、同じように扱えるもののことを指す
- 新しいブラウザ向けに
str.startsWith('prefix')
と書いたら、それが古いブラウザでも同じように動く、みたいな str.startsWithByPolyfill('prefix')
みたいにpolyfillのことを意識して書く必要がない
- 新しいブラウザ向けに
- shimはエミュレーション対象の仕様と同じ機能を持っているものの、インターフェイスが異なるものを指す
- 同じことを実現できるけど、古いブラウザ向けと新しいブラウザでは別々のインターフェイスでそれが実現される
- 最近だと lazysizes が shim の良い例
- 画像の遅延ロードをしてくれるライブラリ
- 新しいブラウザでは
<img src"..." loading="lazy">
が使えるけど、古いブラウザだとJSで動的にスクロール位置を判定して実現しないといけない - これを良い感じにラップして
<img data-src"..." class="lazyload">
で遅延ロードできるようにするというのが lazysizes の仕事 - 古いブラウザでも遅延ロードを実現する都合上、
<img src"..." loading="lazy">
から微妙にインターフェイスが変わってしまっている - こういうのを 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の歴史
- 2010-201?: polyfill黎明期
- jsconfでpolyfillという概念が紹介される
- ついでにHTML5の要素をpolyfillするライブラリをまとめた資料が公開される
- HTML5 Cross Browser Polyfills · Modernizr/Modernizr Wiki · GitHub
- HTML5 Cross Browser Polyfills · Modernizr/Modernizr Wiki · GitHub
- jsconfでpolyfillという概念が紹介される
- 2013: core-js登場 (ref, ref)
- ECMAScriptのビルトインオブジェクトのpolyfillをまとめたライブラリ
Promise
/Map
/Set
など- ES2015の普及と共に広く利用されるように
- 2015?: babel がトランスパイルと同時にpolyfillも自動で仕込んでくれるように
- 2to3 · Babel
- babel の普及とともに広く利用されるように
- その後
babel-polyfill
/@babel/@polyfill
と名前を変えていく
- 2019〜2020:
@babel/@polyfill
が非推奨になり、core-jsを使ってpolyfillを挿入することが推奨されるように- babel-polyfillとcore-jsどちらも殆ど同じことをやっているので片方に寄せた、という経緯
- 7.4.0 Released: core-js 3, static private methods and partial application · Babel
- 2020-05-31のJS: Babel 7.10.0(Babel Polyfills)、Snowpack 2.0、Chrome 84β - JSer.info
現在
- @babel/preset-env + core-jsがデファクト
- browserlist+
useBuiltIns
で必要なpolyfillのみがbundleされるように - https://engineer.recruit-lifestyle.co.jp/techblog/2019-12-08-babel-approach/
- @babel/preset-env · Babel
- browserlist+
- core-jsはECMAScriptのビルトインオブジェクトしか提供してくれない
- そのためブラウザにしか生えていないようなAPIのpolyfillは別で入れる必要がある
fetch
とか
今後
昔と比べてブラウザの自動アップデートが当たり前になり、仕様の標準化が進んでブラウザの差異は解消されつつありますが、全てのユーザに最新のバージョンのブラウザの利用を強制するのが困難なこと、また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が用意される場合もある
- https://github.com/tc39/proposal-temporal
- 実際にユーザに使ってもらって、フィードバックをしてもらうという狙い
おわりに
polyfill の概要の説明から始まり、 閑話休題も挟みつつ、polyfill の今と昔、それから未来について見てきました。polyfill という小さなトピックを取り上げましたが、色々な話が出てきて面白かったですね。そのうち Hatena Developper Blog でも Frontend-Ops をテーマとした記事が色々と出てくると思うので、良かったらそちらもご覧になって下さい。
そうそう、面白い話というと近々弊社のイベントで登壇して、チームにおけるフロントエンドの属人化を頑張って解消していく話をする予定です。近々告知が出る予定ですので、良かったらそちらもご覧になって下さい。 => 告知でました! (2020/12/12 追記)
明日は id:stefafafan さんです。
*1:標準化される前の提案段階の機能のこと。代表的なものとして、ECMAScript の top-level await などがあります: https://github.com/tc39/proposal-top-level-await
*2:こちらも先程紹介した Remy Sharp 氏の記事 が出典です。