SmashingMagのパフォーマンスをどのように改善したか

公開: 2022-03-10
簡単な要約↬この記事では、Webパフォーマンスを最適化し、コアWeb Vitalsメトリックを改善するために、このサイト(Reactを使用したJAMStackで実行)で行った変更の一部を詳しく見ていきます。 私たちが犯したいくつかの間違いと、全面的にすべての指標を後押しするのに役立った予期しない変更のいくつかがあります。

すべてのウェブパフォーマンスの話は似ていますね。 それは常に待望のウェブサイトのオーバーホールから始まります。 完全に洗練され、慎重に最適化されたプロジェクトが開始され、LighthouseとWebPageTestのパフォーマンススコアを上回り、上位にランクインする日。 お祝いと心からの達成感が空中に広がっています—リツイートやコメント、ニュースレター、Slackスレッドに美しく反映されています。

しかし、時間が経つにつれて、興奮はゆっくりと消えていき、緊急の調整、非常に必要な機能、新しいビジネス要件が忍び寄ります。そして突然、コードベースが少し太りすぎて断片化されたサードパーティになります。スクリプトは少し早くロードする必要があり、光沢のある新しい動的コンテンツは、サードパーティのスクリプトとその招待されていないゲストのバックドアを介してDOMに組み込まれます。

私たちはスマッシングにも行ってきました。 それを知っている人はあまりいませんが、私たちは約12人の非常に小さなチームであり、その多くはパートタイムで働いており、そのほとんどは通常、特定の日にさまざまな帽子をかぶっています。 パフォーマンスはほぼ10年間私たちの目標でしたが、専任のパフォーマンスチームが実際に存在することはありませんでした。

2017年後半の最新の再設計後、JavaScript側(パートタイム)のIlya Pukhalski、CSS側(週に数時間)のMichael Riethmueller、そして本当に重要なCSSでマインドゲームをプレイしていました。あまりにも多くのことをやりくりしようとしています。

Lighthouseのスコアが40〜60であることを示すパフォーマンスソースのスクリーンショット
これが私たちが始めたところです。 灯台のスコアが40から60の間のどこかにあるので、私たちはパフォーマンスに(まだ)真正面から取り組むことにしました。 (画像ソース:Lighthouse Metrics)(大プレビュー)

たまたま、私たちは日常の忙しさの中でパフォーマンスを見失いました。 私たちは物事の設計と構築、新製品のセットアップ、コンポーネントのリファクタリング、記事の公開を行っていました。 そのため、2020年後半までに、物事は少し制御不能になり、黄色がかった赤の灯台のスコアが全体的にゆっくりと現れました。 私たちはそれを修正しなければなりませんでした。

それが私たちがいた場所です

すべての記事とページがMarkdownファイルとして保存され、SassファイルがCSSにコンパイルされ、JavaScriptがWebpackでチャンクに分割され、Hugoが静的ページを構築してEdge CDNから直接提供する、JAMStackで実行していることをご存知の方もいらっしゃるかもしれません。 2017年に、Preactを使用してサイト全体を構築しましたが、2019年にReactに移行しました。これを、検索、コメント、認証、チェックアウト用のいくつかのAPIと一緒に使用します。

サイト全体はプログレッシブエンハンスメントを念頭に置いて構築されています。つまり、読者の皆様は、アプリケーションをまったく起動しなくても、すべてのSmashing記事を完全に読むことができます。 それほど驚くことでもありません。最終的に、公開された記事は何年にもわたってあまり変化しませんが、メンバーシップ認証やチェックアウトなどの動的な部分では、アプリケーションを実行する必要があります。

約2500の記事をライブでデプロイするためのビルド全体には、現時点で約6分かかります。 重要なCSSインジェクト、Webpackのコード分割、広告と機能パネルの動的挿入、RSS(再)生成、およびエッジでの最終的なA / Bテストにより、ビルドプロセス自体も時間の経過とともに非常に野獣になりました。

2020年の初めに、CSSレイアウトコンポーネントの大幅なリファクタリングから始めました。 CSS-in-JSやstyled-componentsを使用したことはありませんが、代わりに、CSSにコンパイルされるSassモジュールの古き良きコンポーネントベースのシステムを使用しました。 2017年に、レイアウト全体がFlexboxで構築され、2019年半ばにCSSグリッドとCSSカスタムプロパティで再構築されました。 ただし、一部のページは、新しい広告スポットと新しい製品パネルのために特別な処理が必要でした。 そのため、レイアウトが機能している間はうまく機能せず、保守が非常に困難でした。

さらに、動的に表示したいより多くのアイテムに対応するために、メインナビゲーションのヘッダーを変更する必要がありました。 さらに、サイト全体で頻繁に使用されるコンポーネントをリファクタリングしたかったので、そこで使用されたCSSにも修正が必要でした。ニュースレターボックスが最も顕著な原因です。 ユーティリティファーストのCSSを使用して一部のコンポーネントをリファクタリングすることから始めましたが、サイト全体で一貫して使用されるようにはなりませんでした。

より大きな問題は、非常に驚​​くことではありませんが、メインスレッドを数百ミリ秒ブロックしていた大きなJavaScriptバンドルでした。 大きなJavaScriptバンドルは、単に記事を公開するだけの雑誌では場違いに見えるかもしれませんが、実際には、舞台裏で多くのスクリプトが行われています。

認証された顧客と認証されていない顧客のコンポーネントにはさまざまな状態があります。 サインインしたら、すべての商品を最終価格で表示します。カートに本を追加するときは、どのページを表示していても、ボタンをタップするだけでカートにアクセスできるようにします。 広告は、混乱を招くレイアウトシフトを引き起こさずに迅速に導入する必要があります。同じことが、当社の製品を強調するネイティブ製品パネルにも当てはまります。 さらに、すべての静的アセットをキャッシュし、読者がすでにアクセスした記事のキャッシュバージョンとともに、それらをリピートビューに提供するサービスワーカー。

そのため、このスクリプティングはすべてある時点で行われる必要があり、スクリプトがかなり遅れて届いたとしても、それは読書体験を浪費していました。 率直に言って、私たちはパフォーマンスを注意深く見守ることなく、サイトと新しいコンポーネントに丹念に取り組んでいました(そして、2020年に向けて留意すべき他のいくつかのことがありました)。 思いがけずターニングポイントが来ました。 ハリー・ロバーツは、私たちとのオンラインワークショップとして彼の(優れた)Webパフォーマンスマスタークラスを実行しました。ワークショップ全体を通して、彼は私たちが抱えていた問題を強調し、有用なツールとガイドラインとともにそれらの問題の解決策を提案することにより、例としてSmashingを使用していました。

ワークショップを通して、私は熱心にメモを取り、コードベースを再検討していました。 ワークショップの時点で、Lighthouseのスコアはホームページで60〜68 、記事ページで約40〜60でしたが、モバイルでは明らかに悪化していました。 ワークショップが終わったら、私たちは仕事に取り掛かりました。

ボトルネックの特定

多くの場合、パフォーマンスを理解するために特定のスコアに依存する傾向がありますが、単一のスコアでは全体像が得られないことがよくあります。 David Eastが彼の記事で雄弁に述べているように、Webパフォーマンスは単一の価値ではありません。 それはディストリビューションです。 Webエクスペリエンスが大幅かつ徹底的に最適化されたオールラウンドなパフォーマンスであるとしても、それはただ速くなることはできません。 一部の訪問者にとっては速いかもしれませんが、最終的には他の訪問者にとっても遅くなります(または遅くなります)。

その理由はたくさんありますが、最も重要な理由は、世界中のネットワーク条件とデバイスハードウェアの大きな違いです。 多くの場合、私たちはそれらに実際に影響を与えることができないので、代わりに私たちの経験がそれらに対応することを確認する必要があります。

本質的に、私たちの仕事は、きびきびとした体験の割合を増やし、遅い体験の割合を減らすことです。 しかし、そのためには、分布が実際に何であるかを適切に把握する必要があります。 現在、分析ツールとパフォーマンス監視ツールは必要に応じてこのデータを提供しますが、特にCrUX、Chromeユーザーエクスペリエンスレポートを調べました。 CrUXは、Chromeユーザーから収集されたトラフィックを使用して、時間の経過に伴うパフォーマンス分布の概要を生成します。 このデータの多くは、Googleが2020年に発表したCore Web Vitalsに関連しており、Lighthouseにも貢献して公開されています。

2020年の5月から9月の間に大幅なパフォーマンスの低下を示す最大のContentfulPaint(LCP)統計
2020年の最大のコンテンツフルペイントのパフォーマンス分布。5月から9月の間に、パフォーマンスは大幅に低下しました。 CrUXからのデータ。 (大プレビュー)

全体的に見て、パフォーマンスは年間を通じて劇的に低下し、8月と9月頃に特に低下しました。 これらのチャートを見ると、当時ライブでプッシュしたPRのいくつかを振り返って、実際に何が起こったのかを調べることができます。

ちょうどこの頃、新しいナビゲーションバーをライブで立ち上げたことを理解するのにそれほど時間はかかりませんでした。 そのナビゲーションバー(すべてのページで使用)は、タップまたはクリックでメニューにナビゲーション項目を表示するためにJavaScriptに依存していましたが、実際にはそのJavaScriptビットはapp.jsバンドルにバンドルされていました。 Time To Interactiveを改善するために、バンドルからナビゲーションスクリプトを抽出し、インラインで提供することにしました。

ほぼ同時に、(古い)手動で作成された重要なCSSファイルから、すべてのテンプレート(ホームページ、記事、製品ページ、イベント、求人掲示板など)に対して重要なCSSを生成する自動システムに切り替え、ビルド時間。 それでも、自動生成された重要なCSSがどれほど重いかはわかりませんでした。 私たちはそれをより詳細に調査しなければなりませんでした。

また、ほぼ同時に、プリロードなどのリソースヒントを使用して、Webフォントをより積極的にプッシュしようとして、 Webフォントのロードを調整していました。 ただし、Webフォントがコンテンツのレンダリングを遅らせ、完全なCSSファイルの次に優先順位が高くなっているため、これはパフォーマンスの取り組みに反発しているようです。

さて、リグレッションの一般的な理由の1つは、JavaScriptのコストが高いことです。そこで、JavaScriptの依存関係を視覚的に把握するために、WebpackBundleAnalyzerとSimonHearneのリクエストマップも調べました。 最初はかなり健康に見えました。

JavaScriptの依存関係の視覚的なマインドマップ
画期的なことは何もありません。リクエストマップは最初は過度ではなかったようです。 (大プレビュー)

CDN、Cookie同意サービスCookiebot、Google Analyticsに加えて、製品パネルとカスタム広告を提供するための社内サービスにいくつかのリクエストが寄せられました。 もう少し詳しく調べるまでは、ボトルネックが多いようには見えませんでした。

パフォーマンス作業では、いくつかの重要なページ(ほとんどの場合、ホームページ、ほとんどの場合、いくつかの記事/製品ページ)のパフォーマンスを確認するのが一般的です。 ただし、ホームページは1つだけですが、さまざまな商品ページがたくさんある可能性があるため、オーディエンスを代表するものを選択する必要があります。

実際、SmashingMagでかなりの数のコードが多く、デザインが多い記事を公開しているため、長年にわたって、重いGIF、構文で強調表示されたコードスニペット、CodePen埋め込み、ビデオ/オーディオを含む文字通り何千もの記事を蓄積してきました。埋め込み、および終わりのないコメントのネストされたスレッド。

まとめると、それらの多くは、過度のメインスレッド作業とともに、DOMサイズの爆発的な増加を引き起こし、数千ページのエクスペリエンスを低下させていました。 言うまでもなく、広告を配置すると、一部のDOM要素がページのライフサイクルの後半に挿入され、スタイルの再計算と再描画のカスケードが発生します。これも、長いタスクを生成する可能性のある高価なタスクです。

上のグラフの非常に軽量な記事ページ用に生成したマップには、これらすべてが表示されていませんでした。 そこで、私たちが持っていた最も重いページ(全能のホームページ、最も長いページ、ビデオが埋め込まれているページ、CodePenが埋め込まれているページ)を選び、可能な限り最適化することにしました。 結局のところ、それらが高速であれば、単一のCodePenが埋め込まれているページも高速になるはずです。

これらのページを念頭に置いて、マップは少し異なって見えました。 VimeoプレーヤーとVimeoCDNに向かう巨大な太い線に注意してください。スマッシングの記事から、78件のリクエストが寄せられています。

特に多くのビデオやビデオ埋め込みを使用した記事でのパフォーマンスの問題を示す視覚的なマインドマップ
一部の記事ページでは、グラフの外観が異なります。 特にコードやビデオの埋め込みが多い場合、パフォーマンスは大幅に低下していました。 残念ながら、私たちの記事の多くにはそれらがあります。 (大プレビュー)

メインスレッドへの影響を調べるために、DevToolsのパフォーマンスパネルを詳しく調べました。 具体的には、50ミリ秒より長く続くタスク(右上隅に赤い長方形で強調表示されている)と再計算スタイルを含むタスク(紫色のバー)を探していました。 前者はJavaScriptの実行にコストがかかることを示し、後者はDOMおよび最適ではないCSSへのコンテンツの動的な挿入によって引き起こされるスタイルの無効化を明らかにします。 これにより、どこから始めればよいかについての実用的な指針が得られました。 たとえば、JavaScriptチャンクはまだメインスレッドをブロックするのに十分な重さでしたが、Webフォントの読み込みにはかなりの再描画コストがかかることがすぐにわかりました。

メインスレッドをブロックするのに十分な重さのJavaScriptチャンクを示すDevToolsのパフォーマンスパネルのスクリーンショット
DevToolsのパフォーマンスパネルの調査。 いくつかの長いタスクがあり、50ミリ秒以上かかり、メインスレッドをブロックしました。 (大プレビュー)

ベースラインとして、私たちはCore Web Vitalsを非常に綿密に調べ、それらすべてでスコアが高いことを確認しようとしました。 私たちは特に遅いモバイルデバイスに焦点を当てることを選択しました—物事の悲観的な側面のために、遅い3G、400ms RTT、400kbps転送速度を備えています。 Lighthouseが私たちのサイトにもあまり満足しておらず、最も重い記事に完全に赤一色のスコアを提供し、未使用のJavaScript、CSS、オフスクリーン画像とそれらのサイズについて絶え間なく不平を言っているのも当然です。

機会と推定節約額を示す灯台データのスクリーンショット
Lighthouseは、一部のページのパフォーマンスにも特に満足していませんでした。 それはたくさんのビデオが埋め込まれているものです。 (大プレビュー)

目の前にデータがあれば、重要な(および重要ではない)CSS、JavaScriptバンドル、長いタスク、Webフォントの読み込み、レイアウトシフト、サードパーティに焦点を当てて、最も重い3つの記事ページの最適化に集中できます。 -埋め込み。 後で、コードベースを改訂してレガシーコードを削除し、新しい最新のブラウザ機能を使用します。 先の仕事は大変だったようで、実際、これからの数ヶ月はかなり忙しかったです。

<head>のアセットの順序を改善する

皮肉なことに、私たちが最初に調べたのは、上記で特定したすべてのタスクと密接に関連しているわけではありませんでした。 パフォーマンスワークショップでは、ハリーは各ページの<head>でアセットの順序を説明するのにかなりの時間を費やしました。重要なコンテンツをすばやく配信するには、ソースコードでのアセットの順序について非常に戦略的で注意を払う必要があることを強調しました。 。

重要なCSSがWebパフォーマンスに有益であるということは、今では大きな啓示となるべきではありません。 ただし、リソースヒント、Webフォントのプリロード、同期および非同期スクリプト、完全なCSS、メタデータなど、他のすべてのアセットの順序にどれほどの違いがあるかは、少し驚きました。

<head>全体を上下逆にして、すべての非同期スクリプトと、フォント、画像などのプリロードされたすべてのアセットのに重要なCSSを配置しました。テンプレートによってプリコネクトまたはプリロードするアセットを分類し、ファイルタイプ。特定のタイプの記事とページについてのみ、重要な画像、構文の強調表示、およびビデオの埋め込みが早期に要求されます。

一般に、 <head>の順序を慎重に調整し、帯域幅をめぐって競合するプリロードされたアセットの数を減らし、重要なCSSを正しく取得することに重点を置きました。 <head>の順序に関する重要な考慮事項のいくつかをさらに深く掘り下げたい場合は、ハリーがCSSとネットワークパフォーマンスに関する記事でそれらを強調しています。 この変更だけでも、全体で約3〜4の灯台スコアポイントが得られました。

自動化された重要なCSSから手動の重要なCSSに戻る

ただし、 <head>タグを移動することは話の簡単な部分でした。 さらに難しいのは、重要なCSSファイルの生成と管理でした。 2017年に、すべての画面幅で高さの最初の1000ピクセルをレンダリングするために必要なすべてのスタイルを収集することにより、すべてのテンプレートに対して重要なCSSを手動で手作りしました。 もちろん、これは面倒で少し刺激の少ない作業であり、重要なCSSファイルのファミリー全体と完全なCSSファイルを使いこなすためのメンテナンスの問題は言うまでもありません。

そこで、ビルドルーチンの一部としてこのプロセスを自動化するためのオプションを検討しました。 利用可能なツールは実際には不足していなかったため、いくつかのテストを行い、いくつかのテストを実行することにしました。 私たちはそれらを非常に迅速にセットアップして実行することに成功しました。 出力は自動化されたプロセスには十分であるように思われたので、いくつかの構成を微調整した後、プラグを差し込んで本番環境にプッシュしました。 これは昨年の7月から8月頃に発生しました。これは、上記のCrUXデータの急上昇とパフォーマンスの低下でうまく視覚化されています。 特定のスタイルを追加したり、他のスタイルを削除したりするなどの単純なことで問題が発生することがよくありました。 たとえば、Cookieスクリプトが初期化されていない限り、実際にはページに含まれないCookie同意プロンプトスタイル。

10月に、サイトにいくつかの主要なレイアウト変更を導入しました。重要なCSSを調べたところ、まったく同じ問題が再び発生しました。生成された結果は非常に冗長であり、私たちが望んでいたものではありませんでした。 。 そのため、10月下旬の実験として、私たち全員が強みを結集して、重要なCSSアプローチを再検討し、手作りの重要なCSSがどれだけ小さくなるかを研究しました。 私たちは深呼吸をして、主要なページのコードカバレッジツールの周りで何日も過ごしました。 CSSルールを手動でグループ化し、重要なCSSとメインのCSSの両方の場所で重複とレガシーコードを削除しました。 2017〜2018年に書き戻された多くのスタイルが長年にわたって時代遅れになっているため、これは確かに非常に必要とされていたクリーンアップでした。

その結果、3つの手作りの重要なCSSファイルと、現在進行中の3つのファイルができあがりました。

  • critical-homepage-manual.css(8.2 KB、Brotlified)
  • critical-article-manual.css(8 KB、Brotlified)
  • critical-articles-manual.css(6 KB、Brotlified)
  • critical-books-manual.css(実行する作業
  • critical-events-manual.css(実行する作業
  • critical-job-board-manual.css(実行する作業

ファイルは各テンプレートの先頭にインライン化され、現時点では、サイトでこれまでに使用された(または実際には使用されなくなった)すべてを含むモノリシックCSSバンドルに複製されます。 現在、完全なCSSバンドルをいくつかのCSSパッケージに分割することを検討しているため、雑誌の読者は求人掲示板や本のページからスタイルをダウンロードしませんが、それらのページに到達するとすぐにレンダリングされます重要なCSSを使用して、そのページの残りのCSSを非同期で取得します—そのページでのみ。

確かに、手作りの重要なCSSファイルのサイズはそれほど小さくありませんでした。重要なCSSファイルのサイズを約14%削減しました。 ただし、必要なものはすべて、重複したりスタイルを上書きしたりすることなく、上から最後まで正しい順序で含まれていました。 これは正しい方向への一歩のようであり、灯台をさらに3〜4ポイント押し上げることができました。 私たちは進歩を遂げていました。

Webフォントの読み込みを変更する

私たちの指先でフォントfont-displayすることで、フォントの読み込みは過去に問題になっているようです。 残念ながら、私たちの場合は正しくありません。 親愛なる読者の皆さん、SmashingMagazineの多くの記事にアクセスしているようです。 また、頻繁にサイトに戻って、さらに別の記事を読んでいます。おそらく数時間または数日後、あるいはおそらく1週間後です。 サイト全体で使用されているfont-displayで発生した問題の1つは、記事間を頻繁に移動する読者にとって、フォールバックフォントとWebフォントの間にたくさんのフラッシュがあることに気づいたことです(フォントのように通常は発生しないはずです)。適切にキャッシュされます)。

それはまともなユーザーエクスペリエンスのようには感じられなかったので、オプションを検討しました。 Smashingでは、見出しにMija、本文のコピーにElenaの2つの主要な書体を使用しています。 Mijaには2つのウェイト(RegularとBold)があり、Elenaには3つのウェイト(Regular、Italic、Bold)があります。 Elena's Bold Italicは、ほんの数ページで使用したという理由だけで、再設計中に数年前に削除しました。 未使用の文字とUnicode範囲を削除することにより、他のフォントをサブセット化します。

私たちの記事は主にテキストで設定されているため、サイトでのほとんどの場合、最大のコンテンツフルペイントは記事のテキストの最初の段落または著者の写真のいずれかであることがわかりました。 つまり、最小限のリフローでWebフォントに適切に切り替えながら、最初の段落がフォールバックフォントですばやく表示されるように特別な注意を払う必要があります。

フロントページの最初の読み込みエクスペリエンスをよく見てください(3回スローダウン):

解決策を見つけるとき、私たちは4つの主要な目標を持っていました。

  1. 最初の訪問時に、フォールバックフォントを使用してテキストをすぐにレンダリングします。
  2. フォールバックフォントとWebフォントのフォントメトリックを一致させて、レイアウトのずれを最小限に抑えます。
  3. すべてのWebフォントを非同期でロードし、一度に適用します(最大1回のリフロー)。
  4. その後のアクセスでは、すべてのテキストをWebフォントで直接レンダリングします(フラッシュやリフローはありません)。

最初は、実際にfont-display:swaponfont font-faceを使用しようとしました。 これが最も簡単なオプションのように見えましたが、前述のように、一部の読者は多くのページにアクセスするため、サイト全体でレンダリングしていた6つのフォントで多くのちらつきが発生しました。 また、フォント表示だけでは、リクエストをグループ化したり、再描画したりすることはできませんでした。

もう1つのアイデアは、最初の訪問時にすべてをフォールバックフォントでレンダリングし、次にすべてのフォントを非同期で要求してキャッシュし、その後の訪問でのみキャッシュから直接Webフォントを配信することでした。 このアプローチの問題は、多くの読者が検索エンジンから来ており、少なくとも一部の読者はその1ページしか表示しないことでした。そして、システムフォントだけで記事をレンダリングしたくありませんでした。

では、それでは何ですか?

2017年以来、Webフォントの読み込みに2段階レンダリングアプローチを使用しています。これは、基本的に2段階のレンダリングを記述します。1つはWebフォントの最小限のサブセットを使用し、もう1つはフォントの重みの完全なファミリを使用します。 当時、サイトで最も頻繁に使用されるウェイトであるMijaBoldとElenaRegularの最小限のサブセットを作成しました。 両方のサブセットには、ラテン文字、句読点、数字、およびいくつかの特殊文字のみが含まれています。 これらのフォント( ElenaInitial.woff2およびMijaInitial.woff2 )のサイズは非常に小さく、多くの場合、サイズは約10〜15KBでした。 フォントレンダリングの最初の段階でそれらを提供し、ページ全体をこれら2つのフォントで表示します。

WebフォントのちらつきによるCLS
Webフォントのちらつきが原因のCLS(フォントの変更により作成者の画像の下の影が移動しています)。 レイアウトシフトGIFジェネレーターで生成されます。 (大プレビュー)

これは、どのフォントが正常にロードされ、どのフォントがまだロードされていないかに関する情報を提供するFontLoadingAPIを使用して行います。 舞台裏では、クラス.wf-loaded-stage1本体に追加し、スタイルでこれらのフォントでコンテンツをレンダリングすることで発生します。

 .wf-loaded-stage1 article, .wf-loaded-stage1 promo-box, .wf-loaded-stage1 comments { font-family: ElenaInitial,sans-serif; } .wf-loaded-stage1 h1, .wf-loaded-stage1 h2, .wf-loaded-stage1 .btn { font-family: MijaInitial,sans-serif; }

フォントファイルは非常に小さいので、うまくいけば、それらはネットワークを非常に速く通過します。 次に、読者が実際に記事を読み始めることができるので、フォントの全重量を非同期でロードし、本文.wf-loaded-stage2を追加します。

 .wf-loaded-stage2 article, .wf-loaded-stage2 promo-box, .wf-loaded-stage2 comments { font-family: Elena,sans-serif; } .wf-loaded-stage2 h1, .wf-loaded-stage2 h2, .wf-loaded-stage2 .btn { font-family: Mija,sans-serif; }

したがって、ページをロードするとき、読者は最初に小さなサブセットのWebフォントをすばやく取得し、次に完全なフォントファミリに切り替えます。 現在、デフォルトでは、フォールバックフォントとWebフォントの間のこれらの切り替えは、ネットワークを介して最初に行われるものに基づいてランダムに行われます。 あなたが記事を読み始めたので、それはかなり混乱を感じるかもしれません。 そのため、フォントを切り替えるタイミングをブラウザに任せるのではなく、再描画をグループ化して、リフローの影響を最小限に抑えます。

 /* Loading web fonts with Font Loading API to avoid multiple repaints. With help by Irina Lipovaya. */ /* Credit to initial work by Zach Leatherman: https://noti.st/zachleat/KNaZEg/the-five-whys-of-web-font-loading-performance#sWkN4u4 */ // If the Font Loading API is supported... // (If not, we stick to fallback fonts) if ("fonts" in document) { // Create new FontFace objects, one for each font let ElenaRegular = new FontFace( "Elena", "url(/fonts/ElenaWebRegular/ElenaWebRegular.woff2) format('woff2')" ); let ElenaBold = new FontFace( "Elena", "url(/fonts/ElenaWebBold/ElenaWebBold.woff2) format('woff2')", { weight: "700" } ); let ElenaItalic = new FontFace( "Elena", "url(/fonts/ElenaWebRegularItalic/ElenaWebRegularItalic.woff2) format('woff2')", { style: "italic" } ); let MijaBold = new FontFace( "Mija", "url(/fonts/MijaBold/Mija_Bold-webfont.woff2) format('woff2')", { weight: "700" } ); // Load all the fonts but render them at once // if they have successfully loaded let loadedFonts = Promise.all([ ElenaRegular.load(), ElenaBold.load(), ElenaItalic.load(), MijaBold.load() ]).then(result => { result.forEach(font => document.fonts.add(font)); document.documentElement.classList.add('wf-loaded-stage2'); // Used for repeat views sessionStorage.foutFontsStage2Loaded = true; }).catch(error => { throw new Error(`Error caught: ${error}`); }); }

ただし、フォントの最初の小さなサブセットがネットワークをすぐに通過しない場合はどうなりますか? これは、私たちが望んでいるよりも頻繁に発生しているように見えることに気づきました。 その場合、3秒のタイムアウトが経過すると、最新のブラウザーはシステムフォントにフォールバックし(フォントスタックではArialになります)、 ElenaInitialまたはMijaInitialに切り替えて、後でそれぞれ完全なElenaまたはMijaに切り替えます。 。 それは私たちの試飲で少し多すぎる点滅を生み出しました。 最初は(ネットワーク情報APIを介して)低速ネットワークの最初のステージのレンダリングのみを削除することを考えていましたが、その後、完全に削除することにしました。

そのため、10月に、中間段階とともにサブセットを完全に削除しました。 ElenaフォントとMijaフォントの両方のすべての重みがクライアントによって正常にダウンロードされ、適用の準備ができたら、ステージ2を開始し、すべてを一度に再描画します。 また、リフローをさらに目立たなくするために、フォールバックフォントとWebフォントのマッチングに少し時間を費やしました。 これは主に、ページの最初の表示部分にペイントされた要素にわずかに異なるフォントサイズと行の高さを適用することを意味しました。

そのために、 font-style-matcherと(ahem、ahem)いくつかのマジックナンバーを使用しました。 これが、最初にグローバルフォールバックフォントとして-apple-systemとArialを使用した理由でもあります。 サンフランシスコ( -apple-systemを介してレンダリング)はArialよりも少し優れているように見えましたが、利用できない場合は、ほとんどのOSに広く普及しているという理由だけでArialを使用することにしました。

CSSでは、次のようになります。

 .article__summary { font-family: -apple-system,Arial,BlinkMacSystemFont,Roboto Slab,Droid Serif,Segoe UI,Ubuntu,Cantarell,Georgia,sans-serif; font-style: italic; /* Warning: magic numbers ahead! */ /* San Francisco Italic and Arial Italic have larger x-height, compared to Elena */ font-size: 0.9213em; line-height: 1.487em; } .wf-loaded-stage2 .article__summary { font-family: Elena,sans-serif; font-size: 1em; /* Original font-size for Elena Italic */ line-height: 1.55em; /* Original line-height for Elena Italic */ }

これはかなりうまくいきました。 テキストはすぐに表示され、Webフォントはグループ化された画面に表示されます。理想的には、最初のビューで1回だけリフローが発生し、後続のビューではまったくリフローが発生しません。

フォントがダウンロードされると、 ServiceWorkerのキャッシュに保存されます。 その後の訪問では、最初にフォントがすでにキャッシュにあるかどうかを確認します。 そうである場合は、Service Workerのキャッシュから取得し、すぐに適用します。 そうでない場合は、最初からfallback-web-font-switcherooを使用します。

このソリューションは、フォントをキャッシュに永続的かつ確実に保持しながら、比較的高速な接続でのリフローの数を最小限に抑えました。 将来的には、マジックナンバーをf-modに置き換えることを心から望んでいます。 おそらくザックレザーマンは誇りに思うでしょう。

モノリシックJSの識別と分解

DevToolsのパフォーマンスパネルでメインスレッドを調べたとき、何をする必要があるかを正確に理解していました。 70msから580msの間に8つの長いタスクがあり、インターフェイスがブロックされて応答しなくなりました。 一般的に、これらは最もコストのかかるスクリプトでした。

  • uc.js 、Cookieプロンプトスクリプト(70ms)
  • 着信full.cssファイル(176ms)によって引き起こされるスタイルの再計算(重要なCSSには、すべてのビューポートで高さが1000px未満のスタイルは含まれていません)
  • パネルやショッピングカートなどを管理するためにロードイベントで実行される広告スクリプト+スタイルの再計算(276ms)
  • Webフォントスイッチ、スタイルの再計算(290ms)
  • app.js評価(580ms)

最初に最も有害なもの、つまり最も長いロングタスクに焦点を当てました。

破壊的な雑誌のフロントページのスタイル検証を示すDevToolsから取られたスクリーンショット
下部に、Devtoolsはスタイルの無効化を示しています—フォントスイッチは、再描画が必要な549要素に影響を与えました。 それが引き起こしていたレイアウトの変化は言うまでもありません。 (大プレビュー)

最初の問題は、フォントの変更(フォールバックフォントからWebフォントへ)によって引き起こされた高価なレイアウトの再計算が原因で発生し、(高速のラップトップと高速の接続で)290ミリ秒以上の余分な作業が発生しました。 フォントの読み込みだけでステージ1を削除することで、約80ミリ秒戻ることができました。 50msの予算をはるかに超えていたので、それは十分ではありませんでした。 それで私たちはもっと深く掘り始めました。

再計算が行われた主な理由は、フォールバックフォントとWebフォントの大きな違いによるものです。 フォールバックフォントとウェブフォントの行の高さとサイズを一致させることで、テキストの行がフォールバックフォントの新しい行で折り返されるが、少し小さくなって前の行に収まるという多くの状況を回避することができました。ページ全体のジオメトリに大きな変化を引き起こし、その結果、レイアウトが大幅にシフトします。 letter-spacingword-spacingも試しましたが、良い結果は得られませんでした。

これらの変更により、さらに50〜80ミリ秒を短縮できましたが、コンテンツをフォールバックフォントで表示し、後でWebフォントでコンテンツを表示しない限り、120ミリ秒未満に短縮することはできませんでした。 結果として生じるページビューは、フォントの切り替えによるコストのかかるリフローなしに、Service Workerのキャッシュから直接取得されたフォントでレンダリングされるため、明らかに、初めての訪問者にのみ大きな影響を与えるはずです。

ちなみに、私たちの場合、ほとんどの長いタスクは大規模なJavaScriptではなく、レイアウトの再計算とCSSの解析によって引き起こされていることに気付くことが非常に重要です。つまり、CSSを少し実行する必要がありました。クリーニング、特にスタイルが上書きされる状況に注意してください。 In some way, it was good news because we didn't have to deal with complex JavaScript issues that much. However, it turned out not to be straightforward as we are still cleaning up the CSS this very day. We were able to remove two Long Tasks for good, but we still have a few outstanding ones and quite a way to go. Fortunately, most of the time we aren't way above the magical 50ms threshold.

The much bigger issue was the JavaScript bundle we were serving, occupying the main thread for a whopping 580ms. Most of this time was spent in booting up app.js which contains React, Redux, Lodash, and a Webpack module loader. The only way to improve performance with this massive beast was to break it down into smaller pieces. So we looked into doing just that.

With Webpack, we've split up the monolithic bundle into smaller chunks with code-splitting , about 30Kb per chunk. We did some package.json cleansing and version upgrade for all production dependencies, adjusted the browserlistrc setup to address the two latest browser versions, upgraded to Webpack and Babel to the latest versions, moved to Terser for minification, and used ES2017 (+ browserlistrc) as a target for script compilation.

We also used BabelEsmPlugin to generate modern versions of existing dependencies. Finally, we've added prefetch links to the header for all necessary script chunks and refactored the service worker, migrating to Workbox with Webpack (workbox-webpack-plugin).

A screenshot showing JavaScript chunks affecting performance with each running no longer than 40ms on the main thread
JavaScript chunks in action, with each running no longer than 40ms on the main thread. (大プレビュー)

Remember when we switched to the new navigation back in mid-2020, just to see a huge performance penalty as a result? The reason for it was quite simple. While in the past the navigation was just static plain HTML and a bit of CSS, with the new navigation, we needed a bit of JavaScript to act on opening and closing of the menu on mobile and on desktop. That was causing rage clicks when you would click on the navigation menu and nothing would happen, and of course, had a penalty cost in Time-To-Interactive scores in Lighthouse.

We removed the script from the bundle and extracted it as a separate script . Additionally, we did the same thing for other standalone scripts that were used rarely — for syntax highlighting, tables, video embeds and code embeds — and removed them from the main bundle; instead, we granularly load them only when needed.

Performance stats for the smashing magazine front page showing the function call for nav.js that happened right after a monolithic app.js bundle had been executed
Notice that the function call for nav.js is happening after a monolithic app.js bundle is executed. それは完全に正しくありません。 (大プレビュー)

However, what we didn't notice for months was that although we removed the navigation script from the bundle, it was loading after the entire app.js bundle was evaluated, which wasn't really helping Time-To-Interactive (see image above). We fixed it by preloading nav.js and deferring it to execute in the order of appearance in the DOM, and managed to save another 100ms with that operation alone. By the end, with everything in place we were able to bring the task to around 220ms.

A screenshot of the the Long task reduced by almost 200ms
By prioritizing the nav.js script, we were able to reduce the Long task by almost 200ms. (大プレビュー)

We managed to get some improvement in place, but still have quite a way to go, with further React and Webpack optimizations on our to-do list. At the moment we still have three major Long Tasks — font switch (120ms), app.js execution (220ms) and style recalculations due to the size of full CSS (140ms). For us, it means cleaning up and breaking up the monolithic CSS next.

It's worth mentioning that these results are really the best-scenario- results. On a given article page we might have a large number of code embeds and video embeds, along with other third-party scripts and customer's browser extensions that would require a separate conversation.

Dealing With 3rd-Parties

Fortunately, our third-party scripts footprint (and the impact of their friends' fourth-party-scripts) wasn't huge from the start. But when these third-party scripts accumulated, they would drive performance down significantly. This goes especially for video embedding scripts , but also syntax highlighting, advertising scripts, promo panels scripts and any external iframe embeds.

Obviously, we defer all of these scripts to start loading after the DOMContentLoaded event, but once they finally come on stage, they cause quite a bit of work on the main thread. This shows up especially on article pages, which are obviously the vast majority of content on the site.

The first thing we did was allocating proper space to all assets that are being injected into the DOM after the initial page render. It meant width and height for all advertising images and the styling of code snippets. We found out that because all the scripts were deferred, new styles were invalidating existing styles, causing massive layout shifts for every code snippet that was displayed. We fixed that by adding the necessary styles to the critical CSS on the article pages.

We've re-established a strategy for optimizing images (preferably AVIF or WebP — still work in progress though). All images below the 1000px height threshold are natively lazy-loaded (with <img loading=lazy> ), while the ones on the top are prioritized ( <img loading=eager> ). The same goes for all third-party embeds.

We replaced some dynamic parts with their static counterparts — eg while a note about an article saved for offline reading was appearing dynamically after the article was added to the service worker's cache, now it appears statically as we are, well, a bit optimistic and expect it to be happening in all modern browsers.

As of the moment of writing, we're preparing facades for code embeds and video embeds as well. Plus, all images that are offscreen will get decoding=async attribute, so the browser has a free reign over when and how it loads images offscreen, asynchronously and in parallel.

A screenshot of the main front page of smashing magazine being highlighted by the Diagnostics CSS tool for each image that does not have a width/height attribute
Diagnostics CSS in use: highlighting images that don't have width/height attributes, or are served in legacy formats. (大プレビュー)

To ensure that our images always include width and height attributes, we've also modified Harry Roberts' snippet and Tim Kadlec's diagnostics CSS to highlight whenever an image isn't served properly. It's used in development and editing but obviously not in production.

One technique that we used frequently to track what exactly is happening as the page is being loaded, was slow-motion loading .

First, we've added a simple line of code to the diagnostics CSS, which provides a noticeable outline for all elements on the page.

* { outline: 3px solid red }
* { outline: 3px solid red } 
A screenshot of an article published on smashing magazine with red lines on the layout to help check the stability and rendering on the page
A quick trick to check the stability of the layout, by adding * { outline: 3px red } and observing the boxes as the browser is rendering the page. (大プレビュー)

Then we record a video of the page loaded on a slow and fast connection. Then we rewatch the video by slowing down the playback and moving back and forward to identify where massive layout shifts happen.

Here's the recording of a page being loaded on a fast connection:

Recording for the loading of the page with an outline applied, to observe layout shifts.

And here's the recording of a recording being played to study what happens with the layout:

Auditing the layout shifts by rewatching a recording of the site loading in slow motion, watching out for height and width of content blocks, and layout shifts.

By auditing the layout shifts this way, we were able to quickly notice what's not quite right on the page, and where massive recalculation costs are happening. As you probably have noticed, adjusting the line-height and font-size on headings might go a long way to avoid large shifts.

With these simple changes alone, we were able to boost performance score by a whopping 25 Lighthouse points for the video-heaviest article, and gain a few points for code embeds.

Enhancing The Experience

We've tried to be quite strategic in pretty much everything from loading web fonts to serving critical CSS. However, we've done our best to use some of the new technologies that have become available last year.

We are planning on using AVIF by default to serve images on SmashingMag, but we aren't quite there yet, as many of our images are served from Cloudinary (which already has beta support for AVIF), but many are directly from our CDN yet we don't really have a logic in place just yet to generate AVIFs on the fly. That would need to be a manual process for now.

We're lazy rendering some of the offset components of the page with content-visibility: auto . For example, the footer, the comments section, as well as the panels way below the first 1000px height threshold, are all rendered later after the visible portion of each page has been rendered.

link rel="prefetch" 、さらにはlink rel="prerender" (NoPush prefetch)で少し遊んだことがあります。たとえば、最初のアセットをプリフェッチするために、さらにナビゲーションに使用される可能性が非常に高いページの一部です。フロントページの記事(まだ議論中)。

また、最大のコンテンツフルペイントを減らすために作成者の画像をプリロードし、すべての作成者の画像に使用される踊る猫の画像(ナビゲーション用)や影など、各ページで使用されるいくつかの重要なアセットをプリロードします。 ただし、より正確にするために代わりにNetwork Information APIを使用することを検討していますが、リーダーがたまたま大きな画面(> 800px)にある場合にのみ、これらすべてがプリロードされます。

また、レガシーコードを削除し、多数のコンポーネントをリファクタリングし、 text-decoration-skipの組み合わせで完璧なアンダースコアを実現するために使用していたtext-shadowトリックを削除することで、完全なCSSとすべての重要なCSSファイルのサイズを縮小しました。 -インクテキスト-装飾-厚さ(ついに!)。

やるべきこと

私たちは、サイトのすべてのマイナーな変更とメジャーな変更を回避するためにかなりの時間を費やしました。 デスクトップでの大幅な改善とモバイルでの大幅な向上に気づきました。 執筆時点で、私たちの記事は、デスクトップで平均90〜100のLighthouseスコア、モバイルで約65〜80のスコアを獲得しています。

デスクトップの灯台スコアは90から100の間を示しています
デスクトップのパフォーマンススコア。 ホームページはすでに大幅に最適化されています。 (大プレビュー)
65〜80のモバイルショーでの灯台スコア
モバイルでは、灯台のスコアが85を超えることはほとんどありません。主な問題は、依然としてインタラクティブまでの時間と合計ブロッキング時間です。 (大プレビュー)

モバイルでのスコアが低い理由は、アプリの起動と完全なCSSファイルのサイズが原因で、インタラクティブまでの時間と合計ブロック時間が明らかに低いことです。 ですから、そこでやるべきことがまだいくつかあります。

次のステップとして、現在、CSSのサイズをさらに縮小することを検討しており、JavaScriptと同様に、CSSの一部(チェックアウト、求人掲示板、書籍/電子書籍など)をロードする場合にのみ、モジュールに分割します。必要です。

また、現時点では重要なようですが、 app.jsのパフォーマンスへの影響を減らすために、モバイルで実験をさらにバンドルするオプションについても検討します。 最後に、Cookieプロンプトソリューションの代替案を検討し、CSS clamp()を使用してコンテナーを再構築し、パディングボトム比の手法をaspect-ratio比に置き換え、AVIFで可能な限り多くの画像を提供することを検討します。

それだけです、フォークス!

うまくいけば、この小さな事例研究があなたに役立つでしょう、そしておそらくあなたがあなたのプロジェクトにすぐに適用できるかもしれない1つか2つのテクニックがあるでしょう。 結局のところ、パフォーマンスとは、細部をすべて合計したものであり、合計すると、顧客のエクスペリエンスを向上または低下させます。

私たちはパフォーマンスの向上に全力で取り組んでいますが、アクセシビリティとサイトのコンテンツの改善にも取り組んでいます。 したがって、正しくないことや、Smashing Magazineをさらに改善するために私たちができることを見つけた場合は、この記事へのコメントでお知らせください。

最後に、このような記事の最新情報を入手したい場合は、メールマガジンを購読して、フレンドリーなWebヒント、グッズ、ツール、記事、季節限定のスマッシング猫を入手してください。