BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

Browserslist でサポートブラウザを設定しよう

この記事は BASE Advent Calendar 2021 の 5 日目の記事です。

f:id:yaakaito:20211204005458p:plain

基盤チームの右京です。 最近ひょんなことから browserslist の設定を見返したのですが「babel や autoprefixer で必要になったので導入した」以上はあまり触れられていなかったため、この機会にいちから見直してみようと思いました。

browserslist?

簡単に言えば、クエリを書くとそれに該当するブラウザをリストで取得できます。babel(preset-env) や autoprefixer はここから取得出来るリストを利用して、必要な変換内容を決定しています。単純にバージョン指定でのクエリが記述できるだけではなく、利用統計に基づく絞り込みも可能となっています。例えば、0.2% 以上のシェアがあり、メンテナンスが行われているブラウザは次のクエリで取得できます。

> 0.2%, not dead

このクエリは package.jsonbrowserslist として指定するか、 .browserslistrc を作成して記述すると認識されるようになります。今回は package.json に書く形で進めます。

{
  "private": true,
  "browserslist": [
    "> 0.2%, not dead"
  ]
}

この状態になればブラウザのリストを CLI か JavaScript で取得できます。試しに CLI を使って取得してみます。

$ npx browserslist
and_chr 96
and_ff 94
and_uc 12.12
chrome 95
chrome 94
chrome 93
chrome 92
chrome 87
chrome 61
chrome 49
edge 95
edge 94
firefox 93
firefox 92
ie 11
ios_saf 15
ios_saf 14.5-14.8
ios_saf 14.0-14.4
ios_saf 13.4-13.7
ios_saf 12.2-12.5
ios_saf 9.0-9.2
opera 79
safari 15
safari 14.1
safari 14
safari 13.1
samsung 15.0

babel からの利用

取得できたリストを babel(preset-env) がどのように利用しているかを確認してみます。利用には @babel/preset-env を設定する必要があるので、これを追加して設定します。

{
    "presets": ["@babel/preset-env"]
}

次のようなコードを例に考えてみます。このコードは古いブラウザでは async/await が実装されていないため、このままでは動作できず何かしらの形に変換される必要があります。

(async () => {
    const response = await fetch('https://api.github.com/users/yaakaito');
    console.log(response)
})()

先程の browserslist の結果には async/await を実装していない IE11 が含まれているため、babel で変換すると置き換えがおこるはずです。

> npx babel src/index.js
"use strict";

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

_asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
  var response;
  return regeneratorRuntime.wrap(function _callee$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return fetch('https://api.github.com/users/yaakaito');

        case 2:
          response = _context.sent;
          console.log(response);

        case 4:
        case "end":
          return _context.stop();
      }
    }
  }, _callee);
}))();

このように IE11 でも動作可能なコードに変換されました。ですが、BASE では IE11 のサポートを終了したため、対象ブラウザから IE11 を除外したいと思います。これもクエリを書くことで表現することが出来ます。not IE 11 を追加します。

{
  "private": true,
  "browserslist": [
    "> 0.2%, not dead",
    "not IE 11"
  ]
}

この状態で babel で改めて変換すると、対象にしているブラウザすべてで async/await が実装されているため変換が不要となり、出力されるコードが変化していることがわかります。

$ npx babel src/index.js
"use strict";

(async () => {
  const response = await fetch('https://api.github.com/users/yaakaito');
  console.log(response);
})();

BASE の設定を見直す

さて、きっかけとなった今現在 BASE で設定されている browserslist を見ていきます。browserslist は複数のリポジトリで共有しているため、プライベートな GitHub Packages として配信しており、このような形で利用できるようにしています。

"browserslist": [
    "extends @baseinc/browserslist-config"
]

そしてその中身は...ちょっと頭が痛くなる感じでした。

['ie >= 11', 'safari >= 7', 'iOS >= 10.0', 'and_chr >= 5.0', 'last 1 version', '> 1%']
and_chr 91
and_ff 89
and_qq 10.4
and_uc 12.12
android 91
baidu 7.12
bb 10
chrome 91
chrome 90
chrome 89
edge 91
edge 90
firefox 89
firefox 88
ie 11
ie_mob 11
ios_saf 14.5-14.6
ios_saf 14.0-14.4
ios_saf 13.4-13.7
ios_saf 13.3
ios_saf 13.2
ios_saf 13.0-13.1
ios_saf 12.2-12.4
ios_saf 12.0-12.1
ios_saf 11.3-11.4
ios_saf 11.0-11.2
ios_saf 10.3
ios_saf 10.0-10.2
kaios 2.5
op_mini all
op_mob 62
opera 76
safari 14.1
safari 14
safari 13.1
safari 13
safari 12.1
safari 12
safari 11.1
safari 11
safari 10.1
safari 10
safari 9.1
safari 9
safari 8
safari 7.1
safari 7
samsung 14.0

明らかに内容が古いので、現在推奨している環境に合わせてクエリを見直します。

これをもとに考えると、このようなクエリが適用できそうです。

"browserslist": [
    "> 0.2%, not dead",
    "not IE 11",
    "Safari > 11, iOS > 11"
]

他のツールにも活用したい

最近、TypeScript の変換を速度面の課題から tsc → esbuild に置き換えたいと考えていて、実は今回見直すきっかけになったのもこれでした。Vue.js 2x での TSX サポートのために Babel を通す必要があるのですが、それ以外のものに関してはできれば esbuild のみで解決したい。esbuild の target はブラウザの指定が可能ですが、browserslist には対応していません。

これに browserslist を使えるようにしてみます。すべて対応しようとすると大変なので、ここでは chrome のみで考えます。

#!/usr/bin/env node

const browserslist = require('browserslist');
const { buildSync } = require('esbuild');

const browsers = browserslist();
const target = [browsers.reverse().find(t => t.startsWith('chrome')).replace(' ', '')];
console.log(buildSync({
    entryPoints: ['src/index.js'],
    target
}))

簡易的ですが、browserslist に含まれている chrome の一番古いバージョンを esbuild で解釈できる形に変換しています。これをビルドしてみると、先程と同じ結果になります。

> ./build.js
{ errors: [], warnings: [] }
(async () => {
  const response = await fetch("https://api.github.com/users/yaakaito");
  console.log(response);
})();

試しに async/await が実装されていないバージョン(chrome54)が含まれるクエリに変更してみます。

"browserslist": [
    "> 0.2%, not dead",
    "chrome 54",
    "not IE 11",
    "Safari > 11, iOS > 11"
]
> ./build.js
{ errors: [], warnings: [] }
var __async = (__this, __arguments, generator) => {
  return new Promise((resolve, reject) => {
    var fulfilled = (value) => {
      try {
        step(generator.next(value));
      } catch (e) {
        reject(e);
      }
    };
    var rejected = (value) => {
      try {
        step(generator.throw(value));
      } catch (e) {
        reject(e);
      }
    };
    var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
    step((generator = generator.apply(__this, __arguments)).next());
  });
};
(() => __async(this, null, function* () {
  const response = yield fetch("https://api.github.com/users/yaakaito");
  console.log(response);
}))();

yield を使った結果に変化しました。簡単にではありますが、browserslist から esbuild の設定ができるようになりました。

おわりに

なんとなく設定していた browserslist ですが、最初から見直してみることで考えられる活用の幅が広がったように思います。 今回は、すでに掲載している推奨環境からクエリを逆算しましたが、関係を入替えて browserslist から推奨ブラウザを生成し更新を半自動化するような運用も考えられそうです。

明日は @rry です、お楽しみに!