POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

Miško Hevery

本記事は、原著者の許諾のもとに翻訳・掲載しております。

Google PageSpeed InsightsはWebサイトの知覚的な待機時間を測定できるツールです。 Googleはこのスコアを検索ランキングのアルゴリズムに活用すると公表しているため、良いスコアが必要不可欠です。

私たちは、モバイルサイトのPageSpeed Insightsスコアを100点にするために何が必要かを探ってきました。 この取り組みを始めたとき、デスクトップサイトのスコアはすでに100点でした。 しかし、現代のeコマースの主流はモバイルコマースであり、モバイルサイトでは60点台半ばのスコアしか獲得できていませんでした。 このブログ記事では、モバイルサイトでも100点を取るための方法を共有します。 多くの企業がデスクトップサイトで100点を獲得していますが、モバイルサイトで100点を取れることは滅多にありません。 それでは始めましょう。

Builder.ioは標準的なNext.jsサイトです。 サイト自体がBuilderコンテンツプラットフォームで実行されているため、画像サイズや事前読み込みなど、コンテンツに関するあらゆるベストプラクティスはすでに守られているはずです。 それでもスコアはわずか60点台にとどまっていました。なぜでしょうか?

その理由を探るために、スコアの内訳を見てみましょう。

問題は以下の2つに分解できます。

  • Total Blocking Time(TBT)とTime to Interactive(TTI):JavaScriptによるページ上のブロッキング時間が長すぎる。
  • First Contentful Paint(FCP)とLargest Contentful Paint(LCP):モバイルブラウザにとって、レンダリングが必要なページ上のコンテンツが多すぎる。

そのため、以下を目指す必要があります。

  1. JavaScriptの量を減らす。
  2. 初回のレンダリング時のコンテンツ量を減らす。

なぜJavaScriptの量が増えるのか?

私たちのホームページはほとんど静的なページです。 なぜJavaScriptが必要なのでしょうか?それはホームページがNext.jsサイトであり、つまりReactアプリケーションであるからです(ドラッグ&ドロップエディタによる出力をReactに変換するためにMitosisを使用しています)。 サイトの大部分は静的ですが、JavaScriptが必要な要素が3つあります。

  1. ナビゲーションシステム:メニューにはインタラクティブ性が必要であり、したがってJavaScriptも必要。また、デスクトップとモバイル端末では異なるメニューが使用されている。
  2. チャット用のウィジェットを読み込む必要がある。
  3. Google Analyticsが必要。

それぞれ詳しく見ていきましょう。

アプリケーションのブートストラップ

サイトはほとんど静的であるとはいえ、それでもアプリケーションです。 メニューを有効にするにはアプリケーションをブートストラップする必要があります。 具体的には、フレームワークは再ハイドレーションを実行する中でテンプレートをDOMと比較し、すべてのDOMリスナーをインストールする必要があります。 このプロセスによって、既存のフレームワークはreplayable(再生型)になります。 つまり、たとえページの95%が静的であっても、フレームワークはすべてのテンプレートをダウンロードし、リスナーが存在するかを判断するためにテンプレートを再実行しなければなりません。 これが意味するのは、最初にHTMLとして、次にJavaScriptのJSXとして、サイトが合計2回ダウンロードされるということです。

さらに悪いことに、再ハイドレーションのプロセスは低速です。 フレームワークはDOMの各ノードにアクセスし、仮想DOMとの差分を検出しなければならず、それに時間がかかります。 また、再ハイドレーションのプロセスはDOMリスナーのインストールと同じプロセスなので、遅延させることができません。 再ハイドレーションが遅延すると、メニューは動作しません。

上記は既存のあらゆるフレームワークの根本的な限界を説明したものです。 お分かりのとおり、こうしたフレームワークはすべてreplayableです。 これは、既存のどんなフレームワークも、現実のモバイルサイトで100点を取れないという意味でもあります。 PageSpeed Insightsが想定しているモバイルサイトのHTMLとJavaScriptのコード量はごくわずかであり、それに対して現実のコードがあまりに多すぎるのです。

私たちは問題を根本的に考え直す必要があります。 サイトの大部分は静的なので、その部分のJavaScriptを再ダウンロードしたり、不要な部分の再ハイドレーションに時間をかけたりすべきではありません。 このような場面でこそ、Qwikが真の意味で輝きます。 Qwikはreplayableではなくresumable(再開型)で、それがあらゆる違いを生み出します。 結果として、Qwikでは以下が不要になります。

  1. ページを読み込むときのブートストラップ
  2. リスナーをどこに入れるべきか判断するためにDOMを探索すること
  3. メニューを動かすためにJavaScriptをダウンロード・実行すること

これらはすべて、サイトの読み込みを実行するためのJavaScriptをほとんど必要とせず、それでもサイトのインタラクティブ性を完全に維持できることを意味します。

Intercom

Intercomは、私たちのサイトで実行されているサードパーティウィジェットで、私たちとお客様のインタラクションを可能にします。 このウィジェットをインストールする標準的な方法は、例えば以下のように、JavaScriptをHTMLに少しだけ追記することです。

<script type="text/javascript" async="" src="https://widget.intercom.io/widget/abcd1234"></script>
<script async defer>
Intercom('boot', {app_id: 'abcd1234'}
</script>

しかし、上記のコードには2つの問題があります。

  1. ダウンロードと実行が必要なJavaScriptが増え、TBTとTTIに悪影響を及ぼす。
  2. レイアウトシフトが発生し、CLSに悪影響を及ぼす可能性がある。その原因は、最初にウィジェット以外のUIがレンダリングされ、その後にJavaScriptのダウンロードと実行によってUIがウィジェットとともに再びレンダリングされるためである。 Qwikはそれら両方の問題を同時に解決します。

まず、QwikはIntercomがウィジェットのレンダリングに使用するDOMを取得します。 次に、そのDOMが以下のように実際のページに挿入されます。

<div class="intercom-lightweight-app" aria-live="polite">
  <div
    class="intercom-lightweight-app-launcher intercom-launcher"
    role="button"
    tabIndex={0}
    arial-abel="Open Intercom Messenger"
    on:click='ui:boot_intercom'
  >
    ...
 </div>
 <style id="intercom-lightweight-app-style" type="text/css">...</style>
</div>

この方法のメリットは、ウィジェットがアプリケーションの残りの部分と同時に即座にレンダリングされることです。 ブラウザがIntercomのJavaScriptをダウンロードし、ウィジェットの作成を実行している間、遅延や画面のちらつきは発生しません。 その結果、ユーザ体験が改善され、Webサイトのブートストラップが高速化されます(モバイル端末の通信量も節約できます)。

ただし、ウィジェットがクリックされたことを検知する方法や、ユーザのインタラクションに応じてウィジェットのモックアップを実際のIntercomウィジェットに置き換える何らかのコードは依然として必要です。 これはon:click="ui:boot_intercom"属性によって実現できます。 この属性は、ユーザがウィジェットのモックアップをクリックした場合に、Qwikにboot_intercom.jsをダウンロードさせるものです。

boot_intercom.jsの内容

export default async function(element) {
 await import('https://widget.intercom.io/widget/abcd1234');
 const container = element.parentElement;
 const body = container.parentElement;
 body.removeChild(container);
 Intercom('boot', { app_id: 'abcd1234' });
 Intercom('show');
}

このファイルは、実際のIntercomウィジェットをダウンロードし、モックアップを削除し、Intercomをブートストラップします。 こうした作業はすべて勝手に行われ、ユーザはウィジェットが入れ替わったことにまったく気づきません。

Google Analytics

これまで私たちはJavaScriptの遅延読み込みに取り組み、Webサイトのパフォーマンスを順調に改善してきました。 しかし、アナリティクスは別物です。 なぜなら、アナリティクスを遅延させることはできず、ただちにブートストラップが必要だからです。 アナリティクスをブートストラップするだけで、モバイルサイトのPageSpeed Insightsスコアで100点を取るのは難しくなります。 この問題を解決するために、PartyTownを利用して、Web WorkerでGoogle Analyticsを実行します。 詳しくは今後の記事で説明します。

JavaScriptの遅延

上記の処理は、Webサイトがダウンロード・実行しなければならないJavaScriptの量を約1KBに減らします。 実行にかかる時間はわずか1ミリ秒で、ほとんどゼロと言ってよいでしょう。 このようにJavaScriptを最小限にすることで、TBTとTTIのスコアが完璧になります。

HTMLの遅延

しかし、JavaScriptがほとんど存在しなくても、画面上の領域をレンダリングするためにクライアントに送信するHTMLの量を修正しない限り、依然としてモバイルサイトで100点は取れません。 FCPとLCPを改善するには、とにかくHTMLを最小限に減らさなければなりません。 それを実現する方法は、画面上の領域のHTMLのみを送信することです。

これは目新しいアイデアではありませんが、実行するのは骨が折れます。 アプリケーションを画面上の領域と画面外の領域に簡単に分割する方法がないため、既存のフレームワークでは困難です。 ここでは仮想DOMも役に立ちません。 たとえ表示されるのがほんの一部だとしても、アプリケーションは仮想DOM全体を生成するからです。 フレームワークは、サイトの一部が欠けていると再ハイドレーション時にサイト全体を再生成するため、初回のブートストラップ時の作業がさらに増えます。

理想的には、画面外の領域のHTMLは送信せず、画面上の領域のメニューシステムは完全にインタラクティブな状態に維持したいところです。 しかし実際には、現実のサイトでそのようなベストプラクティスが見られないことから分かるように、実現するのは困難です。難しすぎるので誰もやらないのです。

QwikはDOM主体であり、それがすべての違いを生み出しています。 まずページ全体がサーバでレンダリングされます。 その後、ページの中で送信する必要がない部分が特定され、削除されます。 ユーザのスクロールに応じて、残りの部分が遅れてダウンロードされ、ページに挿入されます。 QwikはステートレスでDOM主体のフレームワークであるため、この種のDOMの操作による影響を受けません。

これは私たちのサーバで、サイトの画面外の領域で遅延読み込みを実現している実際のコードです。

async render(): Promise<void> {
    await (this.vmSandbox.require('./server-index') as ServerIndexModule).serverIndex(this);
    const lazyNode = this.document.querySelector('section[lazyload=true]');
    if (lazyNode) {
      const lazyHTML = lazyNode.innerHTML;
      lazyNode.innerHTML = '';
      (lazyNode as HTMLElement).style.height = '999em';
      lazyNode.setAttribute('on:document:scroll', 'ui:/lazy');
      this.transpiledEsmFiles['lazy.js'] = `
        export default (element) => {
          element.removeAttribute('on:document:scroll');
          element.style.height = null;
          element.innerHTML = ${JSON.stringify(lazyHTML)};
        };`;
    }
  }

コードは簡潔で的確ですが、既存のどのフレームワークでも実現するのは困難でしょう。

以下は、画面外の領域を遅延読み込みするようにした実際のサイトです。

当初、ページは画面外のコンテンツを読み込んでいません。 ユーザがスクロールすると、すぐにコンテンツが追加されます。 ほとんど一瞬でコンテンツが追加されるのは、複雑なコードを実行する必要がなく、高速で単純なinnerHTMLだけで済むためです。

実際に試す

こちらのページを実際に体験してみてください:https://www.builder.io/?render=qwikPageSpeed Insightsのスコアもご覧ください)。 アナリティクスについてはまだ説明していませんが、近いうちに記事を投稿する予定です。

サイトは気に入っていただけましたか? 私たちは、お客様のサイトが超高速に利用できるものとなるように、Builder.ioのすべてのお客様にQwikを提供したいと考えています。 これは今まで見たことがないほど高速なプラットフォームになるはずです。

この記事は面白かったでしょうか?もしそうならば、私たちのチームに参加して、一緒にWebの高速化を目指しませんか。

この記事はこれで終わりですが、私たちは今後数週間にわたって、Qwikとフロントエンドフレームワークの未来について記事を公開する予定です。お楽しみに。

シリーズ記事一覧

info

シリーズ記事一覧はこちらから参照できます。