「ユーザーがブラウザのアドレスバーに 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 に基づいて Render Tree を構築します。
- レイアウトプロセスは各要素ノードの位置とスタイルを計算します。
- 再描画プロセスは実際のピクセルを画面に描画します。
これで、ウェブページがユーザーの前に表示され、次のステップのブラウジングと操作が行われます。
パート 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 にデプロイし、ネットワークリクエストの伝送経路を節約し、リクエスト応答時間を短縮します。
- オーバーライドしないリリースでリソースを更新し、スムーズにアップグレードします。
これで、フロントエンドのエキスパートたちが苦労して書いたコードが、不断のイテレーション、(クラウド)ビルド、成果物リソースのデプロイを経て、外部にリリースされ、全世界のユーザーがインターネット上で私たちの製品を体験し、楽しくサーフィンできるようになります~