「ユーザーがブラウザのアドレスバーに URL を入力した後、何が起こるのか?」この質問は、私たちフロントエンド開発者にとって非常に重要なものであり、フロントエンドの基礎であり、就職面接の定番であり、パフォーマンス最適化の根拠でもあります。しかし、この記事で共有したいポイントは、その後に何が起こるかではなく、その前に何が起こるか、つまり私たちが普段書いているコードがどのようなステップを経て、インターネットユーザーがアクセスできるページになるのか?私たちはどのようにして合理的にウェブページを更新しているのか?
前の質問は開発とデプロイに関するもので、後の質問はリリースに関するものです。以下では、ウェブページのエントリ、開発、デプロイ、リリースの 4 つのパートについて説明します。
パート 1 ウェブページのエントリ#
このパートでは、ユーザーが見るウェブページが何で構成されているのか、ブラウザがこれらの構成要素をどのようにユーザーの前に表示するのかを簡単に紹介します。まず、これが bilibili のメインページです:
内容が豊富で、デザインが美しく、インタラクションが友好的なウェブページは、フロントエンドの三剣士である HTML、CSS、JS、そして画像やフォントなどのリソースファイルなしには成り立ちません:
- HTML はウェブページの内容を決定し、ユーザーが任意のウェブサイトにアクセスするための入口です。HTML 内に直接 CSS、JS コードを書くこともできますし、CSS、JS コードを別のファイルに書いて HTML にインポートすることもできます。
- CSS はウェブページのスタイルに作用します。
- JS はユーザーインタラクションを実現します。
<!-- ウェブページのエントリ HTML の基本構造 -->
<!DOCTYPE html>
<html>
<head>
<title>ウェブページのタイトル、ブラウザで開いたタブに表示される</title>
<meta name="keywords" content="ウェブページのキーワード、SEO"/>
<meta name="description" content="ウェブページの説明、SEO"/>
<!-- html 中のインライン CSS の書き方 -->
<style>
.foo {
color: red;
}
</style>
<!-- html で外部の CSS ファイルをインポートする書き方 -->
<link rel="stylesheet" href="https://s.alicdn.com/@g/msite/msite-rax-detail-cdn/1.0.73/web/screen.css"/>
</head>
<body>
<!-- ウェブページの内容 -->
<div class="foo">
ページの内容
</div>
<!-- html 中のインライン JS スクリプトの書き方 -->
<script>
function log(param) {
console.log(param)
}
log('この JS コードを解析して実行する')
</script>
<!-- html で外部の JS ファイルをインポートする書き方 -->
<script src="https://s.alicdn.com/@g/msite/msite-rax-detail-cdn/1.0.73/web/screen.js"></script>
</body>
</html>
ユーザーが任意のウェブサイトにアクセスする前に、有効なアドレスをアドレスバーに入力する必要があります。その後、ブラウザはサーバーにリクエストを送信し、そのアドレスに対応するウェブページのエントリファイル、つまり「xxx.html」を取得します。ブラウザの Network コンソールを開くと、これがブラウザが最初に受け取ったレスポンス内容であることがわかります。
次に、ブラウザは HTML コードを解析し、他のリソースを認識してさらにリクエストを発行します。さまざまなタイプのリソースのロード、解析、実行(必須ではない)を経て、ユーザーが目にする完全なページが徐々に形成されます。ここで言及しなければならないのは CRP(Critical Rendering Path、重要なレンダリングパス)であり、ブラウザが HTML、JS、CSS コードをユーザーが画面上で見ることができるピクセルに変換するために必要な一連の重要なステップです。以下の通りです:
- ネットワークから HTML をダウンロードし、HTML コードを解析して DOM を構築する。
- ネットワークから CSS をダウンロードし、CSS コードを解析して CSSOM を構築する。
- ネットワークから JS をダウンロードし、JS コードを解析して実行し、DOM または CSSOM を変更する可能性がある。
- DOM と CSSOM が「確定」した後、ブラウザは DOM と CSSOM に基づいてレンダーツリーを構築する。
- レイアウトプロセスは各要素ノードの位置とスタイルを計算する。
- 再描画プロセスは実際のピクセルを画面に描画する。
これで、ウェブページがユーザーの前に表示され、次のステップのブラウジングと操作が行われます。
パート 2 開発段階#
前のパートを見終わった後、ブラウザがウェブページを検索して表示する仕組みがわかったと思います。このパートでは、現代のウェブ開発プロセスを簡単に紹介します。
コードの記述#
ウェブページの内容がますます豊富になり、機能が複雑化する中で、フロントエンドの三剣士である HTML、CSS、JS コードは膨大になっています。明らかに、CSS、JS コードを単一の HTML ファイルにまとめるのはもはや適切ではありません。私たちはもはや従来の方法で直接 HTML、CSS、JS コードを書くことはなく、さまざまな UI フレームワーク(React/Vue/Angular など)を使用してコンポーネントベースの開発を行い、CSS プリプロセッサ(Sass/Less/Stylus など)を使用してスタイルを記述します。
エンジニアリング能力#
フロントエンドビルドツール(webpack/vite/Rollup など)を利用して、さまざまなタイプのファイルを整理し、モジュール化、自動化、最適化、トランスパイルなどのビルド能力を提供し、ローカル開発とプロダクションパッケージングを行います。
ここで、モジュール化について説明する必要があります。モジュール化の利点は、開発段階で異なるタイプのファイルを統一してモジュールとして扱えることです。モジュールはモジュールシステムの第一市民となり、相互に参照できます。異なるファイルタイプのモジュール間の違いは、ビルドツールに解決させます。
import '@/common/style.scss' // scss をインポート
import arrowBack from '@/common/arrow-back.svg' // svg をインポート
import { loadScript } from '@/common/utils.js' // js の関数をインポート
開発段階とは異なり、ビルドツールはプロダクション環境に対しても豊富なビルド能力を提供し、ビジネスソースコードを圧縮、tree-shaking 最適化、uglify 混淆、互換性、extract 抽出などの処理を行い、プロダクション環境に適した最適なコードを生成します。ビルドされたプロダクション環境の js は以下の通りです。
!function(){"use strict";function t(t){if(null==t)return-1;var e=Number(t);return isNaN(e)?-1:Math.trunc(e)}function e(t){var e=t.name;return/(\.css|\.js|\.woff2)/.test(e)&&!/(\.json)/.test(e)}function n(t){var e="__";return"".concat(t.protocol).concat(e).concat(t.name).concat(e).concat(t.decodedBodySize).concat(e).concat(t.encodedBodySize).concat(e).concat(t.transferSize).concat(e).concat(t.startTime).concat(e).concat(t.duration).concat(e).concat(t.requestStart).concat(e).concat(t.responseEnd).concat(e).concat(t.responseStart).concat(e).concat(t.secureConnectionStart)}var r=function(){return/WindVane/i.test(navigator.userAgent)};function o(){return r()}function c(){return!!window.goldlog}var i=function(){return a()},a=function(){var t=function(t){var e=document.querySelector('meta[name="'.concat(t,'"]'));if(!e)return;return e.getAttribute("content")}("data-spm"),e=document.body&&document.body.getAttribute("data-spm");return t&&e&&"".concat(t,".")......
ビルドされたプロダクション環境の css は以下の通りです。
@charset "UTF-8";.free-shipping-block{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-box-align:center;-ms-flex-align:center;-webkit-align-items:center;align-items:center;background-color:#ffe8da;background-position:100% 100%;background-repeat:no-repeat;background-size:200px 100px;border-radius:8px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;margin-top:24px;padding:12px}.free-shipping-block .content{-webkit-box-flex:1;-ms-flex-positive:1;color:#4b1d1f;-webkit-flex-grow:1;flex-grow:1;font-size:14px;margin-left:8px;margin-top:0!important}.free-shipping-block .content .desc img{padding-top:2px;vertical-align:text-top;width:120px}.free-shipping-block .co.....
ビルドツールが出力した HTML コードは自動的に JS、CSS リソースをインポートします:
<!doctype html><html><head><script defer="defer" src="/build/xxx.js"></script><link href="/build/xxx.css" rel="stylesheet"></head><body><div id="root"></div></body></html>
パート 3 コードのデプロイ#
これで、ウェブページのエントリに必要なすべてのリソース(HTML および対応する CSS、JS、その他の静的リソース)を得ることができました。HTML ファイルをダブルクリックしてブラウザで開くことで、ローカルで私たちのページにアクセスできます。ハ!フロントエンドはこんなに簡単です!
次のステップを考えましょう。私たちはテスト、製品、運用、そしてネット上の全世界のユーザーが私たちのページにアクセスできるようにする必要がありますよね?ローカルで動かして遊ぶだけでは(doge)絶対に不十分です。少なくともこれらのリソースをすべてネット上にアップロードする必要があります。
開発段階でのウェブページのアクセスは、ローカルで動作する開発サーバー上で行われ、IP は通常 127.0.0.1 で、ポート番号は任意です。IP + Port + Path の形式でアクセスします。一つは手動でリソースをサーバーにアップロードし、他の人がサーバーの IP + Port + Path でページにアクセスする方法(ウェブサイトのドメイン名の申請、登録、マッピングについては本文では省略します...)、もう一つは専用のリリースプラットフォームを通じてプロセス全体を自動化する方法です。リリースプラットフォームが行うことは、簡単に言うと以下の通りです:
- ブランチのコミット情報、必須設定、依存関係のコンプライアンスチェックなどの一連のチェックを行う。
- スクリプトを実行し、事前に設定された依存関係のインストールおよびビルド指示を実行し、クラウドビルドを開始し、プロジェクトの依存関係をインストールし、プロダクション環境の成果物をパッケージ化します(要するに、クラウドビルドのこのステップは、私たちが先ほど git clone プロジェクトをローカルに初期化して実行し、ローカルでビルドするのと同じです)。
- 成果物を CDN にアップロードします。
これで、ユーザーはブラウザに URL を入力して私たちのページにアクセスできるようになります。サーバーは HTML を返し、HTML 内で CDN 上のリソースを参照し、端末(ブラウザ)がページをレンダリングします。
パート 4 外部へのリリース#
イテレーションと更新#
数万(数百万)の DAU を持つページにとって、膨大なアクセス量と極限のパフォーマンス指標は、正式に外部にアクセスする前にページのイテレーションを修正する際に、安全なリリースとユーザー体験を考慮する必要があります。
.foo {
background-color: red;
}
index.css に関して、ユーザーがページを開くたびにそのファイルへのリクエストを再度発行する必要がある場合、帯域幅を無駄にするだけでなく、ユーザーはダウンロード時間を待たなければなりません。HTTP キャッシュの強いキャッシュを利用して静的リソースをブラウザのローカルにキャッシュすることで、ユーザーはページをより早く見ることができます(早さはブラウザがメモリ / ディストリビューションキャッシュからファイルを直接読み取ることで、ダウンロード時間を省略することに現れます)。
<!-- キャッシュの有効時間を設定 -->
Cache-Control: max-age=2592000,s-maxage=86400
静的リソースに対して、サーバーは通常、キャッシュの有効期限を非常に長く設定してキャッシュを十分に活用します。これにより、ブラウザはリクエストを発行する必要がなくなります。しかし、ブラウザがリクエストを発行しない場合、ページに更新やバグ修正があった場合はどうすればよいのでしょうか?考えやすい方法は、リソースの URL にバージョン番号を付加することです。例えば:
<!-- バージョン番号で更新 -->
<!doctype html>
<html>
<head>
<script defer="defer" src="https://s.alicdn.com/build/foo.js?t=0.0.1"></script>
<link href="https://s.alicdn.com/build/index.css?t=0.0.1" rel="stylesheet">
</head>
<body>
<div class="foo"></div>
</body>
</html>
次回の更新時にバージョン番号を変更すれば、ブラウザに新しいリクエストを強制的に発行させることができます:
<!-- 0.0.2 イテレーションバージョン -->
<!doctype html>
<html>
<head>
<script defer="defer" src="https://s.alicdn.com/build/foo.js?t=0.0.2"></script>
<link href="https://s.alicdn.com/build/index.css?t=0.0.2" rel="stylesheet">
</head>
<body>
<div class="foo"></div>
</body>
</html>
しかし、こうすることには問題があります。HTML が同時に複数のファイルを参照している場合、イテレーションの中で特定のファイルだけが変更された場合、他のファイルが変更されていない場合、すべてのファイルにバージョン番号を付ける方法では、他のファイルのローカルキャッシュも無効になってしまいます!
この問題を解決するためには、ファイル単位の粒度でキャッシュ制御を実現する必要があります。私たちは HTTPS のデータダイジェストアルゴリズムを考え、ファイルの内容に基づいてユニークなハッシュ値を生成し、ファイルが変更されなければハッシュ値は変わらないため、単一のファイルのキャッシュを正確に制御できるようになります:
<!-- ファイル内容のダイジェストによる更新制御 -->
<!doctype html>
<html>
<head>
<!-- foo.js は変更されていないのでキャッシュを引き続き使用 -->
<script defer="defer" src="https://s.alicdn.com/build/foo.js"></script>
<!-- index.css はスタイルが変更されたので、更新されたファイルをリクエストしてキャッシュする必要がある -->
<link href="https://s.alicdn.com/build/index_1i0gdg6ic.css" rel="stylesheet">
</head>
<body>
<div class="foo"></div>
</body>
</html>
または、イテレーションのバージョン番号をリソースパスに追加する方法もあります:
<!-- リソースパスによる更新制御 -->
<!doctype html>
<html>
<head>
<!-- リソースパスが更新され、新しいリソースをリクエスト -->
<script defer="defer" src="https://s.alicdn.com/0.0.2/build/foo.js"></script>
<!-- リソースパスが更新され、新しいリソースをリクエスト -->
<link href="https://s.alicdn.com/0.0.2/build/index.css" rel="stylesheet">
</head>
<body>
<div class="foo"></div>
</body>
</html>
動静分離#
現代のフロントエンドデプロイメントソリューションでは、静的リソース(JS、CSS、画像など)をユーザーに近い CDN にアップロードすることが一般的です。これらのリソースは基本的にあまり変わらず、キャッシュを十分に活用してキャッシュヒット率を向上させる必要があります。一方、動的ページ(HTML)はユーザーデータが千差万別で、SEO のために SSR を行い、パフォーマンスの同構性を保つために、ビジネスサーバーに近い場所に保存され、データの取得や注入がより迅速に行われます。
二種類のリソースが異なる場所に分布している場合、静的リソースは CDN リンクを介して HTML に書かれますが、ページを更新する際には、静的リソースを先にリリースするのか、それともページを先にリリースするのかという問題が生じます。
ページを先にリリースし、静的リソースを後にリリースする場合:
<!-- 新しいページ、古いリソース -->
<!doctype html>
<html>
<head>
<!-- リソースはまだリリースされていない -->
<script defer="defer" src="https://s.alicdn.com/0.0.1/build/foo.js"></script>
<link href="https://s.alicdn.com/0.0.1/build/index.css" rel="stylesheet">
</head>
<body>
<!-- ページが変更された -->
<div class="bar"></div>
</body>
</html>
静的リソースがリリースされる前に、ユーザーは新しいページ構造にアクセスしますが、静的リソースは古いままで、ユーザーはスタイルが崩れたページを目にする可能性があり、古い JS スクリプトが要素ノードを見つけられずにエラーを引き起こす白い画面が表示される可能性があります。これは不可能です 🙅
静的リソースを先にリリースし、ページを後にリリースする場合:
<!-- 古いページ、新しいリソース -->
<!doctype html>
<html>
<head>
<!-- リソースはすでにリリースされている -->
<script defer="defer" src="https://s.alicdn.com/0.0.2/build/foo.js"></script>
<link href="https://s.alicdn.com/0.0.2/build/index.css" rel="stylesheet">
</head>
<body>
<!-- ページはまだリリースされていない -->
<div class="foo"></div>
</body>
</html>
ページがリリースされる前に、ページ構造は変わらず、リソースは新しいものです。もしユーザーが以前にアクセスしたことがあり、ローカルに古いリソースのキャッシュが存在する場合、彼らが見るページは正常です。そうでなければ、古いページにアクセスして新しいリソースを読み込むと、上記のような問題が発生します。スタイルが崩れたり、JS がエラーを引き起こして白い画面が表示されたりすることになります。これも不可能です 🙅
したがって、どちらを先にデプロイするかはどちらも不可能です!これが、昔のプロジェクトがリリースされる際に、プログラマーたちが夜中にこっそりとリリースし、トラフィックの低い時間帯に行う理由です。影響範囲を小さくするためです。しかし、大企業にとっては絶対的な低ピーク期は存在せず、相対的な低ピーク期しかありません。しかし、相対的な低ピーク期であっても、極限を追求する私たちにとっては受け入れられません!
上記の問題は、実際にはオーバーライドリリースによって引き起こされます。リリース待ちのリソースがリリース済みのリソースをオーバーライドすると問題が発生します。これに対する解決策は、オーバーライドしないリリースです。ファイルパスにバージョン番号やファイル名にハッシュを追加し、新しいリソースをリリースする際に古いリソースをオーバーライドしないようにします。静的リソースを全量リリースした後、段階的に全量リリースページを推進することで、問題は完璧に解決されます。
したがって、静的リソースの最適化については、基本的に以下を実現する必要があります:
- 超長いキャッシュの有効期限を設定し、キャッシュヒット率を向上させ、帯域幅を節約する。
- コンテンツのダイジェストやバージョン番号付きのファイルパスをキャッシュ更新の基準として使用し、正確なキャッシュ制御を実現する。
- 静的リソースを CDN にデプロイし、ネットワークリクエストの伝送経路を節約し、リクエスト応答時間を短縮する。
- オーバーライドしないリリースでリソースを更新し、スムーズにアップグレードする。
これで、フロントエンドのエキスパートたちが苦労して書いたコードが、不断のイテレーション、(クラウド)ビルド、成果物リソースのデプロイを経て、外部にリリースされ、全世界のユーザーがインターネット上で私たちの製品を体験し、楽しくサーフィンできるようになります~