徒然技術日記

Object.prototype.__noSuchMethod__

vendor.js の終焉と Granular Chunks

webpack を使った code splitting のベストプラクティスとして,v3 以前の CommonsChunkPlugin の時代から node_modules 以下に置かれている依存ライブラリを vendor.js という単一の chunk にまとめる方法が紹介されていました.

これは webpack の公式ドキュメント Caching | webpackGoogleMake use of long-term caching  |  Web Fundamentals  |  Google Developers でも説明されている通り,「ライブラリのコードはアプリケーションコードに比べると更新頻度が低い」という仮定のもと vendor.js にライブラリコードを切り出すことで,キャッシュ効率をよくすることが主な目的でした.

しかし時は流れ,いつしか「ライブラリのコードは vendor.js に切り出すもの」という習慣だけが根付き,特に理由を考えずに盲目的に実施するものになってしまったように思います(少なくとも私はそうでした.webpack v4 から導入された SplitChunksPlugin の Examples には全てを vendor.js に固めるのはやめた方がよいと書かれているのできちんとドキュメント読みましょうという話ではあるのですが...).SplitChunksPlugin のデフォルト設定が vendor.js を生成するようになっていることもそうした習慣を後押ししたかもしれません.

その間にパッケージの更新を取り巻く状況は大きく変化しました.Greenkeeper,Renovate,Dependabot といった依存パッケージの更新を Pull Request の形で通知してくれるサービスが広まり,場合によっては automerge 機能で半自動的なパッケージ更新まで行えるようになったことで,ここ数年で毎週や毎日などの高い頻度でパッケージを更新する習慣が急速に普及しました.

このような環境では,前述した「ライブラリのコードはアプリケーションコードに比べると更新頻度が低い」という仮定は全く成り立たなくなります.スプリントごとにリリースしている場合にはほぼ確実にリリースごとに vendor.js のキャッシュが無効化されることになりますし,Continuous Deployment で一日に何度もリリースしているプロダクトでもアプリケーションコードと同等以上にライブラリが更新されるでしょう.

vendor.js の利点であったキャッシュ効率のよさがなくなってしまうと,今度はその欠点が目立つようになってしまいます.それは「特定のページでしか使われていないライブラリも vendor.js に固められることで,すべてのページで読み込まれるようになってしまう」という点です.せっかく routes ごとにアプリケーションコードを分割していても,依存ライブラリが同じところに固まっていては片落ちといえるでしょう.

この問題を解決する一つの方法が,Next.js 9.2 で導入された Granular Chunks と呼ばれる設定です.これは GoogleChrome チームによる研究に基づいたより効率的な chunk splitting の方法で,以下のような戦略からなります:

  • 160 KB 以上の依存パッケージは個別の chunk に分ける
  • react, react-dom などのフレームワーク(≠ライブラリ)を一つの chunk にする
  • そのほかの共通モジュールを必要な分だけ切り出す(デフォルトよりもかなり多くの chunk を生成する設定になっている)

実装のより細かい部分や誕生の背景などを含めた詳細については Improved Next.js and Gatsby page load performance with granular chunking を参照いただきたいのですが,これだけでも Granular Chunks が依存ライブラリを過度に特別視することなく可能な限り chunks を分割することで,各 route において必要最小限のコードを読み込むようにしようとしていることがわかるかと思います.

まとめ

  • 依存パッケージを高頻度に更新しているプロジェクトにおいては,vendor.js に依存パッケージを固めるのは却って悪手となる
  • vendor.js のように「一つの chunk に共通モジュールを全てまとめる」という戦略は非効率であり,Granular Chunks はそれに対する一つの解である
    • しかも Google Chrome チームの研究という裏付け付き

Differential Serving の実装に module-nomodule pattern を使うべきではない

タイトルはやや誇張気味ですが,最近某所で differential serving を導入しようとして考えたことのまとめです.

なお,differential serving の概念には CSS や画像なども含まれることがありますし,実現方法にも polyfill.io などの外部ソリューションが存在しますが,本稿では JS の bundle を自前でなんとかする場合に絞って議論することをあらかじめお断りしておきます.

TL;DR

  • module-nomodule pattern は differential serving を始める一歩としてはよいが,長期的には負債になる可能性がある
  • きちんとやるなら UA sniffing なり feature detection なりで分岐させるべき

Differential Serving とは

そもそも differential serving とは何かについて簡単に説明します.知っている方は読み飛ばして大丈夫です.

Differential serving とは Progressive Transpilation, Smart Bundling などとも呼ばれるテクニックで,ES2015+ を解釈できるブラウザと解釈できないブラウザとで配信する JS を変えるというものです.

2016年,@babel/preset-env の登場によって我々はターゲットブラウザを指定して柔軟にかつ自動的にトランスパイルの度合いを変更できるようになりました.

しかしながら @babel/preset-env は spec のサポートが最も貧弱なブラウザに合わせてトランスパイルを行うため,サポート環境に Internet Explorer のような古いブラウザが含まれているといくら Firefox/Chrome/Safari が最新の spec を実装しても最終的に ES5 のコードへ変換されてしまう状況が続いてしまいます.

すると本来は不必要なトランスパイルによりコードサイズが増えたり ES2015+ に合わせた JS エンジン側の最適化の恩恵が受けられなかったりなどの問題が発生し, @babel/preset-env の smart さを充分に発揮できなくなってしまいます.

そこで考えられたのが なんとかして ES2015+ のコードが実行できる環境なのかどうかを判別し,使えるブラウザには最低限のトランスパイルだけしたバンドルを配信すればいいのでは? というアイデアです.

これにより Netflix では 20-40%, Google のエンジニア Philip Walton の実験では minified & gzipped で 50% 以上もバンドルサイズを削減できたと報告され,注目が集まりました.

Module-nomodule Pattern とは

Differential serving を実装する方法はいくつかありますが,その中でも "module-nomodule pattern" と呼ばれている手法は Deploying ES2015+ Code in Production Today — Philip Walton にて紹介されたもので,その実装の簡単さから広まりつつあります.

そもそも differential serving の実現には次の二つの課題を解決する必要があります.

  1. ES2015+ が実行できるかどうかをどう判別するか
  2. 判別した結果に応じて異なるバンドルをどう配信するか

Module-nomodule pattern では ESModulesサポートしているブラウザと主要な ES2015+ の構文をサポートしているブラウザがほとんど被っていることに注目し,<script type="module"><script nomodule> を使って a., b. の両方の問題を同時に解決します.

上記二つの script 要素があった場合,ESModules のサポート状況によってブラウザは排他的にどちらかを読み込みます.例えば

<script type="module" src="modern.mjs" />
<script nomodule src="legacy.js" />

という二つの要素があると,読み込まれるスクリプトは以下のようになります.

ESModules のサポート modern.mjs legacy.js
🙆‍♂️ 読み込む 読み込まない
🙅‍♂️ 読み込まない 読み込む

ここで,ES2015+ のサポート状況を合わせるとこうなります.

ES2015+ のサポート ESModules のサポート modern.mjs legacy.js
🙆‍♂️ 🙆‍♂️ 読み込む 読み込まない
🙅‍♂️ 🙅‍♂️ 読み込まない 読み込む

つまり,判別問題と配信問題を一挙に解決できるのです.

あとは modern.mjs を「ESModules をサポートしているブラウザが理解できる構文だけになるようトランスパイルする」ように設定して生成するだけです.幸いなことに @babel/preset-env の targets にはこのための専用の設定である targets.esmodules が実装されているので,設定を true にするだけでよくブラウザを列挙する手間はかかりません.また上で説明した ESModules 読み込みの排他制御部分にバグがあるブラウザが若干存在するのですが,それを自動的に解決する Webpack プラグイン webpack-module-nomodule-plugin もあります.

まさに至れり尽くせり!これで全て解決!といきたいのですが,この方法には長期的な視点で見ると問題があります.

Module-nomodule Pattern の問題点

お気づきの方もいると思いますが,この方法ではターゲットブラウザが「ESModules をサポートしているブラウザ」に固定されてしまいます.具体的には https://github.com/babel/babel/blob/master/packages/babel-compat-data/data/native-modules.json で定義されているブラウザです.

これらブラウザの下限バージョンは,原理上今後どんなに新しいブラウザがリリースされようと変わることがありません.つまりこれでは新たな "babel-preset-es2015 問題" あるいは "last 2 versions 問題" とでも言うべき状態になってしまいます.2 年後の 2022 年,Chrome のバージョンが 100 の大台に乗る頃になってもずっと Chrome 61 をサポートし続けなくてはならないということはないでしょう.

せっかく @babel/preset-env と differential serving でターゲットを柔軟にカスタマイズして配信できる体制を整えようとしているのに,歴史を繰り返す必要はありません.

Module-nomodule Pattern を使わない Differential Serving の実装方法

では module-nomodule pattern を使わない場合にはどのように differential serving を実装したらよいでしょうか? これには二つのアプローチがあります.

Feature Detection

Netflix にて紹介された 方法で,以下のような ES2015 で導入された構文を使ったコードを eval で実行して a. の判別問題を解決します.

この方法は,テストに使用するコードを変更することで柔軟に feature detection を行える点,および一般にアンチパターンとされる UA sniffing を避けられる点が利点です(Netflix では sniffing と併用していたようですが).

一方で欠点としては browserslist ベースのツール (@babel/preset-env や autoprefixer など) との相性があまりよくない点が挙げられます.これらのツールは「サポートするブラウザーのリスト」を指定する必要があるので,「ブラウザーリスト→ ES の構文」という順序で判断が行われます.一方 feature detection では変換対象の ES の構文を決めてビルドしておき,その構文をブラウザがサポートしているかどうかを実行時に判断します.つまり,考える方向が真逆になっているのです.(反対に,TypeScript のコンパイラ tsc のように出力先の ES version を指定して変換するようなツールでは feature detection と相性がよいと思われます)

一方で b. の配信問題についてはスライド中で特に紹介されていませんが,Cookie を使った制御が出てくるところからおそらくバンドル生成時に app.bundle.es5.js と app.bundle.es6.js のようにファイル名を分けておき,Cookie や feature detection の結果からどちらを配信するか判断していたものと考えられます.

User-Agent Sniffing

愚直に User-Agent を見て出し分ける方法です.Feature detection の項で説明したとおり browserslist ベースのツールと相性がよい一方,tsc と一緒に使うには少し工夫が必要と思われます.

ビルド時に指定したターゲットブラウザと同じ分け方でファイルを出し分けるようにすれば a, b の問題はともに解決できることになります.

具体的には,CDN・サーバーサイド・クライアントサイドのいづれかの場所で browserslist に指定したクエリと同じブラウザ振り分けを UA ベースで実現すれば OK です.Node.js や クライアントサイドでは ua-parser-js などが,VCL では @financial-times/useragent-parser または @financial-times/polyfill-useragent-normaliser などが使いやすいと思います(これらの parser や normaliser では browserslist の "last 2 versions" のような指定ができないため,ターゲットブラウザの更新方法など運用を工夫する必要はあります).

github.com github.com github.com

おわりに

Feature detection や User-Agent sniffing と比べると配信部分にほとんど手を加える必要がないという点で module-nomodule pattern の手軽さは際立っています.そのため「まず differential serving を使ったチューニングの第一歩として導入する」というケースでは重宝されるでしょう.

一方で長期的な視点ではこの記事で書いたような問題を孕んでいるため,数年後に技術的負債となる可能性も考えられます.今後も継続的に differential serving によるアプリケーション提供を続けていく予定があるのであれば,運用を含めたより堅牢な実装を検討するメリットはあるのではないかと思います.


『エンジニアのためのマネジメントキャリアパス』から見る SHIROBAKO

この記事は SHIROBAKO Advent Calendar 2019 - Adventar 24日目の記事です.

はじめに

はじめに少し自分語りをさせてください.

このアドベントカレンダーには初参加ですが,歴代の多くの参加者と同じく,私も学生の時に SHIROBAKO を見て今は社会人となった人のうちの一人です.社会人二年目で Web フロントエンドエンジニアとして働いています.

チームリーダーから割り当てられた案件をこなすことで精一杯だった一年目と異なり,二年目の今年は2-3人の小規模なチームを率いての開発や,インターン生のメンター,同じチームに配属となった新卒のトレーナーをするなど,マネジメントスキルも問われる一年間でした.

そんな中で出会ったのが『エンジニアのためのマネジメントキャリアパス』という本(以下単に「本」と略します)です.発売されてから界隈で話題になったのでご存知の方もいらっしゃるかもしれませんが,この本には書名の通り「エンジニアがジュニアエンジニア→テックリード→ CTO とマネジメントのキャリアラダーを上がっていく場合,どのようなスキルが必要なのか?」が書かれています.初耳だという方には, 『エンジニアのためのマネジメントキャリアパス』読了 に内容がよくまとめられていますが,ぜひ書籍も手にとってみてもらいたい本です.

五年間にわたる SHIROBAKO Advent Calendar でも繰り返しテーマとなってきたエンジニア(ディベロッパー)と SHIROBAKO の類似性ですが,「マネジメントキャリアパス」という観点から各登場人物を分析してみるとどうなるのか,新人(第一章)とメンター(第二章)に絞って試してみたいと思います.

新人(第一章)

本の第一章では新人が上司とどのように関わっていけばよいかについて書かれています. SHIROBAKO では,我らが主人公おいちゃんを筆頭として,太郎,絵麻,ずかちゃん,みーちゃん,つばき,沙羅,愛など数多くの新人が登場します.

本で新人が上司に対して求めるべきこととして掲げている「1on1」「フィードバック」「トレーニングとキャリアアップ」についてそれぞれ見てみましょう.

まず,Web 業界ではそれなりに浸透してきたと思われる「1on1」ですが,武蔵野アニメーションやスーパーメディア・クリエイションズには導入されていないようです.1on1 は主に「上司と部下の間の信頼関係を築くこと」「要検討事項について上司と定期的に1対1で話す場を設けること」を目的として実施されます.太郎が胸の内に秘めてしまった案件のこじれ,おいちゃんやみーちゃんのキャリアパスに関する悩み,絵麻の作画に関する悩みなどは 1on1 が有効な場面といえそうです.作中では別の方法で解決していましたが,仕組みとして 1on1 を導入できれば悩みをもっと気軽に相談できるようになるかもしれません.特に,矢野さん-おいちゃん,井口さん-絵麻,平岡-太郎のコンビはよい 1on1 ができそうです.

「フィードバック」については,矢野さん→おいちゃん,瀬川さん→絵麻,小笠原さん→井口さんなど数多く描写されています.特に7,8話での瀬川さんから絵麻への原画に対するネガティブフィードバックは好例です.本にもある通りこれは「自分の態度や行動に関するフィードバックをもらうことに慣れていない新入社員にとっては相当居心地の悪い経験」だったと思いますが(絵麻は真面目なので「居心地の悪い」どころではなさそうでしたが),井口さんに相談した経験や,その後の瀬川さんから直接もらえた修正に対する褒めの言葉は,絵麻を大きく成長させたことと思います.

「トレーニングとキャリアアップ」については,職種や人物によって異なりそうです.

もっともしっかり考えられていそうなのはアニメーター陣です.ポジション(動画・原画・キャラデザ・作画監督など)やカットに要求される能力の有無が仕事内容に直結するある意味「技術職」だからというのもあると思いますが,個々人の能力や得手不得手を,アニメーター間のみならず監督・制作進行・ラインPなど他職種の人もしっかり把握している印象を受けました.例えば,井口さんをキャラデザに任命するときの小笠原さんとおいちゃんによる説得や,SHIROBAKO イントロダクション (JUMP j BOOKS)で描かれた絵麻の原画昇格試験のシーンから,個人の持っている能力と成長見込みを丁寧に評価して担当を割り振っていることがわかります(杉江さんの場合は社内の誰も把握できていなかったようですが...).さらに,小笠原さんの「微力ながらサポートさせていただきます」「井口さんを抜擢したのなら,ちゃんと相談に乗ったりアドバイスしてあげて下さい」という言葉は,アニメーター陣に普段から育成の文化が存在していることを示していそうです.

一方,制作進行組についてはあまりよくわからないというのが正直なところです.キャリアパスについて悩んでいるおいちゃんに対してメンタリングしたり,漠然と「監督になる」と宣言しているだけの太郎に対して具体的に今どのような能力を伸ばすべきかという詳細なアドバイスをしているようには見えません.ただ,本田さん,矢野さん,ナベ P が適宜仕事の進め方について助言をしたり,(人員不足という側面もあるにせよ)おいちゃんにデスクを任せたりしているように,(主人公=おいちゃん目線ではわからないだけで)先輩陣は後輩のキャリアアップの道を考えてくれていそうです.

また,みーちゃんのいたスーパーメディア・クリエイションズでは残念ながらあまりキャリアについて考慮されている様子はなさそうに思えます.第9話の「私なんて三年くらいずーっと車の内装ばっかりやってる」「俺は五年.車の外装だな」という先輩達の言葉から,車の中でもさらに一つの部分に特化したスペシャリストを育成するような人員割り振りを行なっているのだと思われます.立石社長は「この先,能力や適性に応じて他の CG も作ってもらう」とみーちゃんに話しているように,能力が上がれば他のことも任せられるようになることを示唆してはいるのですが,おそらく適性を見極めた後は同じ部品の制作に特化するようになるのではないでしょうか.このことは会社自体の「車の 3D に特化する」という戦略とも一貫性があるものの,自身の対外的なスキルアップという点では厳しそうです.実際,タイヤ CG に関する技能があることがスタジオカナブンの中垣内社長に伝わっていなかったことからも,前職のスキルを元にした転職活動ではなかったことがわかります.しかし,これはいわゆるブラック企業的発想(人的リソースは使い潰すもの)というところからきたわけではなく,あくまで会社の生存戦略として意図的にそうしており,福利厚生も手厚い企業であることが立石社長を含めた複数人から語られています.みーちゃんがキャリア相談を立石社長に直にしたときにも,立石社長は親身になって相談に乗り,みーちゃんが本当は何がしたいのか?ということを考えるきっかけを提供してくれています.

この「本当は何がしたいのか?」「最終的に自分はどうなりたいのか?」という自分のキャリア上の望みは,上司に頼るべきキャリア上の相談と異なり,自分の責任で考え抜いて明らかにするべきことだと本に書かれています.この点は SHIROBAKO の作品全体を通したテーマである「何のためにアニメを作るのか」と重なることもあって,作中でも多くの人物の「望み」が描かれています(2年前の Advent Calendar 参加記事 キャリアパスどうするねん問題 - アナログ金木犀 の分析が詳しいです).個人的には,パティシエという夢を叶えた本田さん,自分のやりたいことと業界のミスマッチから去っていった竹中・辻(どちらもSHIROBAKO イントロダクション (JUMP j BOOKS)より)など,必ずしもアニメ業界に留まる人ばかりではないというところが作品の魅力をあげていると思います.

メンター(第二章)

第二章ではインターンシップ生や新人のメンターになったときの心構えについて書かれています.どちらかというとインターンに関する記述(タスクの選び方など)の比重が大きいためそのままムサニに適用できることが多いとは言い難いですが,新人への接し方には共通点を見つけられそうです.

まず,「新人の目を通して業務を見直す」という点について,若干意味は異なるものの近い場面として杉江さんが絵麻に「人に教えるということは、自分にも教えるということ」「言葉にして伝えることで、改めて分かることもあるし」「自分がちゃんと理解してないと、教えられないからね」と話すシーンが挙げられます.今まで教わってきたこと,やってきたことを後輩に言語化して伝えることで,自分の中でも業務内容や大事にしていること(コアバリュー)を整理できたことでしょう.作中では明確に描写されてはいませんが,おいちゃんもつばきと沙羅に指導していて同じような気持ちを抱いたに違いありません.

また,多人数が関わるクリエイティブな業務であるため,「傾聴」の大切さはメンター・メンティー間に限らず繰り返し登場していました.2話でおいちゃんが監督へあるぴんの嗜好を尋ねるシーン,9話で舞茸さんが監督と言葉のキャッチボールをしながら最終話の展開を詰めるシーン,13話でおいちゃんが真っ先に監督へ作品の方向性や表現したいことを尋ねるシーン,絵麻と愛との全体的なやりとりなど,挙げればキリがなく,どの人も傾聴力が高くて羨ましい限りです.反対に,20話までの平岡は,傾聴力が足りずに失敗したケースと言えるでしょう.平岡はおいちゃんが指摘した通りアニメーターとのコミュニケーションが不足していましたが,周囲の人物もまた,ムサニ内外に関わらず平岡とのコミュニケーションが不足していたと考えられます.

おわりに

年末進行で SHIROBAKO をじっくり見返す時間が取れなかったので薄い考察になってしまったのは否めず,年末年始の時間でまた見返したい欲が高まりました.テックリード(第三章)と本田デスクや作画監督,組織のリーダー(第四章〜)と丸山社長,立石社長などの社長陣の関係についても,時間ができたらぜひ考察してみたいと思っています.

ありがとうございました!

個人のポータルサイト nodaguti.github.io を作りました

各種アカウントへのリンクをまとめたポータルサイトとしてずっと about.me というサービスを使っていたのですが,発表スライドなども載せたくなったのと,フロントエンドエンジニャーとしてはこれくらい自前で作らないといけないのではという思いがずっとありました.

何回か作りかけては面倒になって放置していたのですが,今回やっと完成しました.

nodaguti.github.io

Next.js と GitHub Pages でシンプルに作りました.各サービスに載せていた URL も更新しています.

ブログまで引っ越そうとすると Blosxom で作っていたときのようにまた無限に時間が溶けてしまう予感がしたので,一旦はポータル用のペライチだけとなっています. i18n 対応は気が向いたらするかもしれないです. i18next & react-i18next あたりがよさそうに感じています.

作りが簡単なので苦労した点も特にはありませんが,とりあえず完成したというご報告でした.

【電子版あり】RxJS 6に対応した問題集を技術書典7で頒布しました

先日開催された技術書典7にて AbemaTV の有志が集まって出版した AbemaTV Tech Book に参加し, AbemaTV で学ぶ RxJS と題して RxJS の記事を寄稿しました!

techbookfest.org

(久々の投稿が宣伝で申し訳ないです)

非同期処理やイベントを効率的に,かつ宣言的に処理することのできる RxJS は,Angular にとどまらず React など他のフレームワークを使用した Web アプリケーションを構築する際にも便利なライブラリです.

しかしながら,RxJS には 100 以上の operator と呼ばれるストリームを操作するためのメソッド群が存在するため,学習コストが高めであり,興味はあってもなかなか RxJS の世界に足を踏み入れられていない方も多いのではないでしょうか.

それらの膨大に存在する operator を使いこなせるようになるためには,さまざまなロジックを RxJS で自ら表現してみる経験を積むのが一番です.ところが,RxJS について解説している記事はそこそこ存在するものの,個々の operator に関する説明が多く,それらを組み合わせて一つのロジックを構築する部分の学習題材が不足しているのではないかと感じていました.

そこで今回,実際に開発の現場で遭遇したシチュエーションを問題形式に仕立て,練習問題集としてみました.ひととおり operator についてざっと学んだ後,手に馴染ませるための補助として活用していただければ嬉しく思います.

書典の会場では完売となったため,BOOTH にて電子版を販売しています.下の「試し読み版」では1問目をみることができますので,まずはそちらに挑戦してみてください!(該当の記事は第2章です)

speakerdeck.com

abematv.booth.pm

願わくば,他の章もご覧いただき,AbemaTV のエンジニアの幅の広さを感じ取っていただけるととても嬉しいです.

使われていないCSSルールを検出する stylelint-no-unused-selectors を作った

github.com

背景

発端はあるツイートより.

たしかに CSS Modules は webpack の恩恵により,styled-components や emotion の場合にはその仕組みゆえに,どれも CSS の dead code elimination (使われていない CSS ルールが最終的な CSS ファイルに入らないようにすること) は自然と行われます.

一方で, CSS in JS なアプローチを取らない場合には往々にして challenging なものとなります.既存のツールとしては UnCSS, DropCSS, PurifyCSS, PurgeCSS などがあるものの,以下のような理由から自作することにしました.

  • コンポーネントごとに CSS ファイルを分けて書いている場合 に使いづらいものが多かった
    • 入力として HTML しか対応していないもの (UnCSS, DropCSS), アプリケーション全体で使われていない CSS を検出するもの (PurifyCSS, PurgeCSS) など微妙にユースケースが合わなかった
    • コンポーネントごとに1対1で対応するテンプレートファイルと CSS ファイル間でチェックしたかった
  • jsx, tsx, CSS Modules, classnames にも対応したい
  • コードを書いているときにリアルタイムで警告されてほしい
    • stylelint の仕組みに乗っかるのが簡単そう

というわけで,以下のようなコンポーネントがある場合に,FooComponent.css と FooComponent.jsx のクラスを比較して, jsx 側で使われていないクラスがあったときに警告を出す stylelint プラグインである stylelint-no-unused-selectors が完成しました.

FooComponent
├── index.js
├── FooComponent.jsx
└── FooComponent.css

使い方

stylelint と stylelint-no-unused-selectors をインストール.

yarn add -D stylelint stylelint-no-unused-selectors

その後 .stylelintrc に設定を追加すれば動くようになります.詳しくは README を参照してください.

{
  "rules": {
    "plugin/no-unused-selectors": true
  }
}

stylelint のプラグインがエディタで有効になっていれば,未使用の CSS を発見した場合に以下のようなエラーが出るはずです.

stylelint-no-unused-selectorsによりエディタで未使用CSSが検出されている様子

ターミナルで実行するとこんな感じ.

ターミナルでstylelintを実行している様子

コード解析には @babel/parser と typescript を使っているので,TC 39 stage 4 未満の機能を使っている場合や,TypeScript で書かれている場合もサポートしています.

ちなみに600近くのコンポーネントがある某プロダクトでも試してみましたが,問題なく動作しました.

技術的な話

AST を本格的に扱うのは初めてだったので,どれから始めていいのかわからず途中まで acorn で作ってから後で @babel/parser (babylon) に移行するなど迷走してました. この界隈は玄人向けなのかあまりドキュメントがなく,かといって AST の仕様書やコードを直接読むのはかなりしんどいので,AST Explorer でいろいろいじくりまわして雰囲気で進めるのがよさそう.

JS/TS と比べると PostCSS 界隈はさらに情報が少なく,既存のプラグインのコードから挙動を推測する感じだったのが辛かったです...

おわりに

publish してから時間がなかなか取れずに記事を書けないでいたところ,なんと PostCSS の公式 Twitter が紹介してくれたのにはとても驚きました.

しっかりしたドキュメントを書くことの重要性を改めて感じました(なかなか面倒臭がってしまいますが...).記事も英語版をどこかに投稿しないと,と思っています.

主に自分たちの課題を解決するために作り始めたプラグインですが,もし便利に思っていただける方がいましたらとても嬉しく思います!

web.dev の Fast load times を読んだ

パフォーマンス改善のための基礎知識をつけるべく,Chrome Dev Summit 2018 で紹介された web.dev に書かれているドキュメントのうちパフォーマンスに関するもの

web.dev

を一通り読んだので,簡単なまとめとして読書メモを書いた.

デブサミには幸運にも会社から派遣メンバーとして選ばれて参加したが,全体的にパフォーマンスに関する話題が多く,Google としてもパフォーマンス向上に力を入れていることが強いメッセージとして伝わってきた. web.dev はその啓蒙活動の一環としてドキュメントと Lighthouse の Web 版が提供されている.

Discover performance opportunities with Lighthouse

  • lighthouse は metrics (現在のページのパフォーマンスの状態を表す指標) と opportunities (パフォーマンスの改善ポイント) を提供する
  • web.dev または Chrome の DevTools から実行できる
  • スコアについての詳細は https://developers.google.com/web/tools/lighthouse/v3/scoring に書かれている
  • https://web.dev/measure から Profile にサイトを登録することで, daily に Lighthouse のレポートを測定してくれるようになる

Use Imagemin to compress images

  • imagemin で適切に画像を圧縮する話
  • ビルド時に毎回圧縮するのは無駄が多いので,画像追加時に一回だけやりたい
    • PR 時に適切に圧縮されているかどうか調べたい
    • GitHub Actions でチェックできるかも?

Replace animated GIFs with video for faster page loads

  • ffmpeg で gif を webm/mp4 に変換して video タグに置き換えればファイルサイズがずっと小さくなる(例では1/10になっている)

Use lazysizes to lazyload images

  • 画像を lazyload することについて
  • Intersection Observer の活用を頑張らなければ...

Serve Responsive Images

  • viewport の大きさごとに最適なサイズの画像を配信することについて
  • sharpImageMagick を使うことで画像のリサイズが可能
  • img 要素の srcset 属性で width descriptors を使うことでブラウザに画像サイズを伝えることができる
    • ブラウザ側が最適な大きさの画像を取得してくれる
    • バイスの解像度に応じて画像を変える density descriptors もある
  • width descriptors を使う場合は sizes 属性も同時に使う必要がある
    • 画像が表示される大きさをブラウザに伝える
  • picture 要素を使うとより細かい制御が行える

Serve images with correct dimensions

  • 適切な画像サイズをどう判断したら良いかについて
  • まずは不適切なサイズの画像を探す: Lighthouse の "Properly size images" audit で調べられる
  • Good approach
    • 絶対単位で大きさが指定されている場合
      • 表示されるサイズにリサイズする
    • 相対単位で大きさが指定されている場合
      • どのデバイスでも適切に表示されるサイズにリサイズする.デバイスのスクリーンサイズは GA などから判断できる
  • Better approach
    • 絶対単位で大きさが指定されている場合
      • srcsetsizes 属性を使ってデバイスの解像度に応じた適切な画像が配信されるようにする
        • おそらく density descriptors のことを指している
    • 相対単位で大きさが指定されている場合
      • srcsetsizes 属性を使ってスクリーンサイズに応じた適切な画像が配信されるようにする
        • おそらく width descriptors のことを指している

Use WebP images

  • webp は 25-35% ファイルサイズが小さく,YouTube ではページ読み込みが 10% 早くなった
  • cwebp または Imagemin で生成可能
  • picture 要素を使って配置する
  • Lighthouse の "Serve images in next-gen formats" audit で調査可能

Apply instant loading with the PRPL pattern

  • critical なリソースを preload する
    • <link rel=preload>
    • リソースのリクエスト優先度をあげることができる
  • First Paint をできる限り早く render する
  • Service Worker を用いてアセットを pre-cache する
  • 必要ないリソースは lazyload する
    • Code splitting することで JS を lazyload できる
      • 重要度の高い chunk は preload の設定をする
    • 画像も lazyload できる

Preload critical assets to improve loading speed

  • <link rel=preload> で優先度を上げられる
    • ブラウザがリソースを発見するのに時間がかかるものの first paint に重要なリソースに対して使うと効果が高い
      • 例: @font-face に指定されるリソースは CSS のパースが終わらないとブラウザが認識できない
    • webpack は /* webpackPreload: true */ を使うと preload を挿入できる
  • <link rel=prefetch>: 現在のページのリクエストが全て完了した後に,次のページ遷移を高速化するための準備を定義できる
    • /* webpackPrefetch: true */ もある

Reduce JavaScript payloads with code-splitting

  • Code-splitting について
  • route もしくは component レベルで splitting するのがシンプルな方法

Remove unused code

  • Lighthouse の "Unused JavaScript" audit で実行されなかったコードを解析できる
  • webpack-bundle-analyzer でバンドルに含まれるモジュールを可視化できる
  • import する対象を限定したり,ライブラリ自体を使わないようにすることでコード量を減らせる

Minify and compress network payloads

  • Minification
    • UglifyJS
  • Data compression
    • gzip, brotli
    • 大抵 CDN がやってくれることが多いはず
    • Dynamic compression
      • リクエストされたタイミングで圧縮する
        • シンプルだがレスポンスを返すのに余計な時間がかかる
    • Static compression
      • 事前に圧縮しておく
        • BrotliWebpackPlugin, CompressionPlugin などの webpack plugin がある

Serve modern code to modern browsers for faster page loads

  • @babel/preset-env
  • <script type=module>
    • preset-env で targets: esmodules: true を設定すると ESM が使えるブラウザのみをターゲットにできる
    • type=module をセットすれば ESM を解釈できるブラウザのみを対象にファイルを配信できる

Avoid invisible text during font loading

  • "flash of invisible text" の代わりに "flash of unstyled text" を目指す
  • font-display: swap; を使う
    • 対応ブラウザが限られる
  • フォントファイルが読み込まれるまで system font を適用しておき,読み込まれたことをトリガーにして custom font を当てるようにする

Using the Chrome UX Report to look at performance in the field

  • CrUX はオプトインしたユーザーから収集した実際の First Contentful Paint (FCP), DOM Content Loaded (DCL), First Input Delay (FID) のデータセット
  • CrUX Dashboard, PageSpeed Insights, BigQuery の3つ手段でデータにアクセスできる
  • CrUX Dashboard
    • Data Studio を使ったパフォーマンスの時間変化を可視化できるツール
  • PageSpeed Insights
    • URL を指定すると直近30日間のデータを集約して表示する
    • API もある
  • BigQuery
    • 自前でいろいろ分析したい時に

Using the CrUX Dashboard on Data Studio

  • Data Studio の Community Connectors を基に作られているツール
  • https://g.co/chromeuxdash にページの origin URL を入れれば dashboard を作成できる
  • dashboard は FCP, デバイス分布,接続状況分布から構成されている
  • FCP
    • Fast (<1s), Average (1~2.5s), Slow (>2.5s) の比率が表示される
  • バイス分布
  • 接続状況分布
    • 4G, 3G, 2G, Slow 2G, Offline の比率が表示される
  • これ以外のメトリクスや国ごとのデータなどを見たい場合は BigQuery を使う必要がある

Using the Chrome UX Report on PageSpeed Insights

  • PageSpeed Insights は Lighthouse と CrUX の結果を一度に見られるようにしたもの
  • Lab data (Lighthouse の結果) と Field data (CrUX のデータ) に分かれて表示される
  • Field data には FCP と FID が Fast, Average, Slow に分けて表示される
    • FCP: Fast (<1s), Average (1-2.5s), Slow (>2.5s)
    • FID: Fast (<50ms), Average (50-250ms), Slow (>250ms)
    • 入力したページのデータの他に, origin の結果(そのページが属する origin の全てのページのデータを集約したもの)も表示される
    • PSI のデータは過去30日のもの.これは月単位でデータを集約する BigQuery とは集計の仕方が異なっているので注意する必要がある
  • PSI は以下の利点がある
    • ページ単位でのパフォーマンスが見られる唯一のツール
    • 日単位でデータを集計してくれる唯一のツール
    • API がある
  • その代わり過去のデータは見られないし,メトリクスも FCP と FID しかない

Using the Chrome UX Report on BigQuery

  • SQL を使って生データを解析できる
  • 国ごとのデータ,月ごとのデータがテーブルとして提供されている
  • 無料の範囲は1ヶ月あたり 1TB で,それ以降は $5/TB の料金がかかる

Performance budgets 101

  • performance budgets
    • パフォーマンスに影響するメトリクスに対する制限
    • 設定した制限が,機能追加・デザイン・技術選定・ライブラリ選定を行う際の判断基準の一つになる
  • 定量的なメトリクス
    • 開発の初期段階で有効
      • サイズが大きくなるのを防ぐことができる
    • 画像サイズ,Web フォントの数,JS のサイズ,外部リソースの数, etc.
    • これらはユーザーエクスペリエンスを直接表していないので注意する必要がある
      • 例えばサイズが小さくても critical path のスクリプトの読み込み優先度が低かったらユーザーになかなか内容が表示されなくなってしまう
  • Milestone timings
  • ルールベースのメトリクス
    • Lighthouse, WebPageTest が提供するスコア
  • どうやって目標値を決定するのか?
    • まずは計測してみる.競合のサイトのスコアも計測してみよう.
    • もしそうした調査にかける時間がない場合には,TTI 5秒以下,critical path のリソースが 170KB 以下というのが一つの目安
      • これは 3G の通信環境を考慮に入れて考えられた数字
  • performance budget の値はページごとに異なりうる
  • 使えるツール
  • 継続的に数値を追うのがよい

Your first performance budget

Performance Budget の決め方について

  1. 最も重要なページがどこかを決める
  2. そのページのメトリクスを Lighthouse で測定する
  3. 競合のメトリクスを調査する
  4. 10サイトくらいは調査した方がいい
  5. milestone timings の budget を決める
  6. 20% ルールが有効
    • レスポンスタイムに 20% の違いがあるとユーザーが気がつくという研究に基づく
  7. initial はまず現在の値から 20% 改善を目指し,最終的には競合より 20% よい値を目指す
  8. 他の種類の budget を決める
  9. 定量的メトリクス
    • 一般的に critical path のリソースは 170KB 以下に保つべき
      • 低スペックで 3G な環境でも快適になる
      • 4G をターゲットにするならもう少し余裕ができる(表を参照
    • コンテンツの種類によっても変わってくるので,最終的な値はその辺りを考慮して決める必要がある
      • e-commerce で画像が多いなら JS の制限をきつくするなど
  10. ルールベースのメトリクス
    • Lighthouse で 85 点以上など
  11. 優先度を考える
  12. ニュースサイトであれば FCP, 検索サイトであれば TTI など重要なメトリクスはサイトによって異なる
  13. CrUX で競合サイトの状況を見てみるのもよい

Incorporate performance budgets into your build process

Performance Budget をビルドプロセスに組み込む方法について

  • Webpack Performance Hints
    • 非圧縮のサイズなのに注意
    • ただし圧縮は転送速度を早めるだけで,特にモバイルで顕著に影響する parse の速度を早めることはないので,非圧縮のサイズを気にすることも大事
  • Bundlesize
    • CLI で使えるほか,CI に組み込める
    • gzip, brotli, none から圧縮方法を選択して,そのサイズに対する budget を設定できる
  • Lighthouse Bot
    • Travis でしか使えないが,budget に違反している PR を block できる

Using bundlesize with Travis CI

  • Travis で bundlesize を使う方法
    • このページは正直 bundlesize の ReadMe を見れば充分 😗

Using Lighthouse Bot to set a performance budget

  • Lighthouse Bot を使う方法
    • localhost ではなくどこかのサーバーに deploy した方がリアルなデータが得られる