在瀏覽器輸入網址並送出後,到底發生了什麼事?
26 Nov 2018這道題目涵蓋前端技術範圍很廣,很適合在各階段拿來梳理並檢視自己的技能和狀態。那麼,就讓我來檢視一下自己的狀態吧 d(`・∀・)b
…
當使用者在瀏覽器網址列輸入網址並按下 Enter 後會發生什麼事情呢?分為以下重點說明…
一、瀏覽器的內部運作機制
瀏覽器架構
瀏覽器(在這裡是指 Chrome)是多程序(multi-process)的架構,意即當開啟一個應用程式時,會由一個以上的 process 聯合運作來完成任務,並且 process 又可將工作切分給其下的執行緒(thread)來協助運作,其中 process 與 process 間是利用 IPC(inter-process communication)的方式來溝通。
瀏覽器內的 process 有哪些呢?
- browser process:瀏覽器功能,例如:網址列、書籤、送出網路請求等。
- renderer process:網頁顯示,基本上每個 tab 擁有自己獨立運作的 renderer process(註 1),甚至每個 iframe 也是由個別的 renderer process 來負責執行(註 2),但在某些情況下多個 tab 會合用一個 render process,例如:多個空白 tab、記憶體吃緊。
- plugin process:控制網頁所用的 plugin,例如:flash。
- GPU process:處理跨核心的工作與 3D 圖形繪製。
不同的 process 可切分為不同的 thread 以負責不同的功能…
- browser process 包含 ui thread、network thread。
- renderer process 包含 main thread、worker thread。main thread 負責剖析 HTML 字串並產生 DOM tree,同時若發現需下載的資源,就會透過 IPC 通知 browser process 的 network thread 來發出網路請求以取得該資源;或使用加速的 preload scanner 方法,意即在剖析 HTML 的同時掃描是否有需要下載的資源,若有就透過 IPC 通知 browser process 的 network thread 來發出網路請求以取得該資源,剖析與下載檔案平行進行。稍後會提到阻礙渲染的原因和解法。
因此,當使用者在瀏覽器網址列輸入網址並送出後,browser process 的 ui thread 接收到使用者輸入的資料,ui thread 會要求發出網路請求,此時 network thread 開始執行 DNS lookup、建立連線,而當連線成功、資料傳輸完畢後,network thread 通知 ui thread 資料已準備好,browser process 利用 IPC 將資料傳遞給 render process 以進行頁面渲染,渲染完畢即可讓使用者看到畫面。
更多關於瀏覽器內部運作機制的資訊,可參考這裡。
備註
- 註 1:每個 render process 有配置其獨立的記憶體空間,因此除了必要的資源外並不共享,安全性較高。
- 註 2:目前 Chrome 可做 site isolation,意即每個 iframe 也是由獨立運作的 renderer process 來負責執行,而這些 render process 又有分配獨立的記憶體空間,對於安全性來說,隔離不同網站的記憶體使用的資訊,可說是同源策略(same-origin policy)的第二層保護機制。
解析 URL
所謂的「網址」即是「統一資源定位符」(Uniform Resource Locator,URL),可唯一標註資源的所在位置,其格式為 協定類型:[//伺服器位址[:埠號]][/資源層級UNIX檔案路徑]檔案名[?查詢][#片段ID]
,例如:https://www.sample.com/main/page/index.html?keyword=abc&happy=abc#heading-1
。也就是說,當使用者在瀏覽器網址列鍵入網址,瀏覽器的 ui thread 接收到輸入的資料,network thread 便開始將這網址字串進行解析以便下載資源,而這個網址字串會解析成以下部份…
- 協定類型:http、https、ftp 等。
- 伺服器位址:這裡可以放域名(domain name)或 IP address,若為域名就需要做 DNS 解析,例如:
www.sample.com
。 - 埠號:預設為 80,若為預設就不用寫出來。
- 資源層級 UNIX 檔案路徑,例如:
/main/page/
。 - 檔案名:
index.html
。 - 查詢(query string):以問號
?
開始,並以&
分隔,例如:?keyword=abc&happy=abc
。 - 片段 ID(fragment):以 hash 開頭,常用於定位,例如:
#heading-1
。
減少網路請求的數目
關於以上過程,可做的優化有
- 減少網路請求的數目,其實這裡牽涉到「最佳化關鍵轉譯路徑」,這個「關鍵轉譯路徑」不是指像素管道,而是指關鍵資源的優化,其步驟為 (1) 分析及描述關鍵路徑:資源數量、位元組數、長度;(2) 盡量減少關鍵資源數量:刪除相應資源、延遲下載、標記為非同步資源等;(3) 最佳化剩餘關鍵資源的載入順序:儘早下載所有關鍵資源(preload、prefetch),以縮短關鍵路徑長度;(4) 盡量減少關鍵位元組數,以縮短下載時間 (往返次數)。點此看範例。
- DNS 解析優化,包含 DNS 快取、DNS 負載平衡與 DNS Prefetch,稍後在網路連線的部份會再詳述。
先來看如何減少網路請求的數目。
若無 HTTP/2,則由於網路資源請求耗時耗力、每次對於每個網域所能請求的連線數有限,因此必須減少網路請求的數目,例如:使用 webpack 等工具有系統的打包、瀏覽器快取的應用、圖片資源的整合(sprites、base64、各種 SVG 的應用)等。
關於這部份可參考以下筆記
- 如何減少 HTTP Requests 的數量?含文字或圖片資源整合、外部檔案與內聯推送。
- 文字或圖片資源整合,例如:利用 webpack 有系統的打包,再配合瀏覽器快取機制便能更降低 HTTP 請求次數。
- 圖片資源的整合,例如:sprites、base64、各種 SVG 的應用,同樣再配合瀏覽器快取機制便能更降低 HTTP 請求次數,但注意在 HTTP/2 下就不再需要 sprites 了。
- 外部檔案的合併和引入位置的調整,避免阻塞 DOM Parsing。
- 內聯推送,例如:CSS in JS、inline JavaScript。
- 快取,例如:記憶體快取、Service Worker 快取、HTTP 快取與 Push 快取。
二、網路連線
URL 解析完畢後,若伺服器位址的地方是放置域名而非 IP address 就需要做 DNS 解析以得到 IP,接著再建立 TCP/IP 連線來傳輸檔案。備註,由前面所述可知,網路連線是 single thread 的,一個請求即使用一個 network thread。
DNS 解析
DNS(domain name server)用於提供域名和 IP address 互相轉換的資訊。由於 DNS lookup 耗時耗力,因此若有快取就用快取,若無快取則至少要做 DNS Prefetch 提早處理。
關於 DNS 解析過程中可做的優化是
- DNS 快取:可做瀏覽器暫存(例如:查看 Chrome 的 DNS 快取
chrome://net-internals/#dns
,點此看詳細資訊)、系統暫存、路由器暫存、IPS 伺服器暫存、根域名伺服器暫存、頂級域名伺服器暫存或主域名伺服器暫存。 - DNS 負載平衡:讓多個 IP 位置能對應到同一個域名,好處是避免 ISP 業者所提供的 DNS 服務更新不夠即時或連線中斷時主動切換成正確或備援的 IP 位置並即時更新上游 DNS 的 IP 資訊,讓服務能穩定連線不中斷,可參考這裡。
- DNS Prefetch:若能提早做 DNS 解析,就能讓使用者有速度變快的錯覺,可參考-dns-prefetch。
建立 TCP/IP
TCP/IP(Transmission Control Protocol/Internet Protocol)是網路上的一種通訊協定,用於在不同設備或環境間傳送訊息。另,HTTPS 是指將 HTTP 使用 SSL 加密後再利用 TCP 發送,加強安全性。TCP/IP 會經由三次握手建立連線,再經由四次揮手斷開連線。
由於瀏覽器對於同一 domain 的 TCP/IP 連線數是有限制的,且每個檔案請求皆需一個 TCP/IP 連線,因此針對這個議題有許多優化方案,主要目的都是為了減少請求數,點此回顧減少網路請求的數目的方法。
GET vs POST
HTTP 請求方法有 GET、POST、PUT、DELETE 等,以下主要探討最常用的 Get 與 Post。
- Get:瀏覽器將 headers 與 data 同時送出,因此只會產生 1 個 TCP 封包,最後伺服器端回應 200 與資料。
- Post:瀏覽器先將 headrs 送出,等到伺服器端回應
100 continue
後再傳送 data,因此會產生 2 個 TCP 封包,最後伺服器端回應 200 與資料。
TCP/IP 五層網路架構
當瀏覽器發送網路請求時,會從應用層經層層通訊協定到實體層傳送出去,而當對方收到後,再反向經由實體層的層層通訊協定到應用層解讀訊息。
這五層依序是…
- 應用層(application):提供各種網路應用程式,例如:WWW、FTP、Email 等
- 傳輸層(transport):提供點對點的接口,例如:TCP、UDP。
- 網路層(internet):為封包決定路由、尋找地址,例如:路由器。
- 資料連結層(network):將位元組成位元組,再將位元組組成幀,使用連結層的位址來訪問介質,並做差錯檢測。
- 實體層(physical):提供實體網路上的傳輸服務,例如:乙太網路、光纖網路等。
OSI 七層網路架構
另外一種架構方式是使用 OSI 七層網路架構,與 TCP/IP 五層網路架構可互相對應。
推薦閱讀-什麼是 OSI 的 7 層架構?和常聽到的 Layer 7 有關?。
三、伺服器處理請求並返回 HTTP 回應
客戶端發送請求後,伺服器收到請求會進行處理並回應,而 HTTP 請求或回應訊息的結構是:方法、通用標頭、請求或回應標頭、請求或回應身體。
通用標頭
格式如下
- Request URL:請求位置。
- Request Method:請求方式,例如:GET、POST、OPTIONS、PUT、HEAD、DELETE、CONNECT、TRACE 等。。
- Status Code:狀態碼,例如:200 成功。
- Remote Address:請求遠端伺服器位址,會轉為 IP。
範例如下,這是我最近常買東西的網站-蝦皮拍賣。
Request URL: https://shopee.tw/
Request Method: GET
Status Code: 200
Remote Address: 103.117.4.201:443
Referrer Policy: no-referrer-when-downgrade
狀態碼
列舉常見的狀態碼如下。
- 1XX:參考資訊,例如:POST headers 後得到 100(繼續)、101(切換通訊協定)。
- 2XX:成功,例如:200 客戶端要求成功。
- 3XX:重新導向,例如:301 永久導向、302 暫時導向、304 未修改。
- 4XX:用戶端錯誤,例如:401 拒絕存取、403 禁止使用、404 找不到。
- 5XX:伺服器錯誤,例如:500 伺服器錯誤。
請求與回應標頭
請求標頭(request headers)
- :authority:要連接的遠端主機和接口訊息。
- :method:連接方法。
- :path:請求網址的檔案路徑和查詢字串。
- :scheme:請求網址的協議規範,例如:
https
。 - accept:能夠接受的回應內容類型(content-types),例如:
accept: text/plain
。 - accept-encoding:能夠接受的編碼方式列表,這裡是指 HTTP 縮壓,例如:gzip、deflate 等。
- accept-language:能夠接受的回應內容的自然語言列表,例如:de(德語)、en(英語)。
- cache-control:用來指定在這次的請求或回應中的所有快取機制都必須遵守的指令,例如:
cache-control: no-cache
。 - cookie:由伺服器透過 set-cookie 所代入的文字串。
- upgrade-insecure-requests:指示伺服器若遇到
http
時要改為https
。 - user-agent:瀏覽器的瀏覽器身分標識字串,例如:瀏覽器產品名。
:authority
、:method
、:path
、:scheme
是 HTTP/2 的偽請求標頭(pseudo-header field),並不屬於 HTTP 正規的欄位,因此必須出現在正規欄位之前。
範例如下。
:authority: shopee.tw
:method: GET
:path: /
:scheme: https
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
accept-encoding: gzip, deflate, br
accept-language: zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7
cache-control: max-age=0
cookie: _gcl_au=1.1.802438797.1542279133; csrftoken=PRD6fx4beV7XOXgma6JOyHYwEi0AtHi7; SPC_F=3wuKDJImIN5pTDXzt7VKSLa3hQCPOv6M; REC_T_ID=83fc55f6-e8c4-11e8-91a5-f8f21e1a4b50; SPC_SI=c9fuhx7aqz5hr2l5ztuu2izcqd4kz9v3; ...
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36
回應標頭(response headers)
- cache-control:定義快取策略,例如:no-cache, no-store(決定是否要求更新檔案)、public, private, must-revalidate(可快取回應的使用者)、max-age(快取時限)。
- content-encoding:在資料上使用的編碼類型。
- content-type:當前內容的 MIME(檔案性質與格式)類型。
- date:伺服器發送資料的時間。
- etag:資源的版號,用於識別檔案是否有更新。
- expires:指定日期或時間,超過這個時間的資源就判定為過期。
- last-modified:資源的最後更新時間。
- server:伺服器名稱。
- status:伺服器的回應狀態碼,例如:200 OK。
- vary:告知下游的代理伺服器,應當如何對未來的請求協定頭進行符合,以決定是否可使用已快取的回應內容而不是重新從原始伺服器請求新的內容。
範例如下。
cache-control: no-cache
cache-control: no-cache, no-store
content-encoding: gzip
content-type: text/html
date: Tue, 20 Nov 2018 04:53:31 GMT
etag: W/"5bed4437-d09"
expires: Tue, 20 Nov 2018 04:53:30 GMT
last-modified: Thu, 15 Nov 2018 10:02:31 GMT
server: SGW
status: 200
vary: Accept-Encoding
請求標頭與回應標頭是一一對應的,例如…
- 請求標頭的 accept 和回應標頭
content-type: text/html
必須要能對應,意即 content-type 必為 accept 當中其一類型。 - 跨域請求的請求標頭的 origin 與回應標頭 access-control-allow-origin 必須要能對應,意即 origin 必為 Access-Control-Allow-Origin 當中其一網域。
- 暫存的請求標頭的 if-none-match 對應回應標頭的 etag。
請求與回應身體
這裡是指實體的訊息,目前多為 json 格式,內容為 html 字串,瀏覽器接收到就會解析並渲染。
範例如下。
CRLF
斷行符號(CRLF,carriage-return line-feed),在請求標頭和實體訊息之間有一個 CRLF 作為分隔,而回應標頭和回應實體之間也有一個 CRLF 作為分隔。
不同的平台下,斷行符號可能是不同的…
- Windows:CRLF,
\r\n
或0x0D 0x0A
。 - Unix:LF,
\n
或0x0A
。 - Mac:CR,
\r
或0x0D
。
在使用 Git 等版本控管工具時很常發生這個問題-Git 在 Windows 平台處理斷行字元 (CRLF) 的注意事項。
cookie
cookie 是本地端的一種儲存方式,用於搭配客戶端的 cookie 與伺服器端的 session 以進行資料通訊功能。關於 cookie、session、localStorage、sessionStorage 的說明與比較可參考這裡。
在同域名的資源請求時,瀏覽器會自動帶入本地端的 cookie,而這些 cookie 可能非常多而減低下載速度,因此需要做優化-域名拆分,例如:sample.com 下的資源可拆分為 a.sample.com 與 b.sample.com,這樣當瀏覽 a.sample.com 下的頁面時,就不會帶入 b.sample.com 的 cookie。但注意域名解析需要時間,過多域名可能會降低下載速度,建議可用 DNS 解析的優化方案來解決。
Gzip 壓縮
由伺服器開啟壓縮方式,除了 Gzip 壓縮外,還有 Brotli 等,在壓縮文字資源後可大量減少檔案體積,加快傳輸速度。
點此看 Gzip 筆記。
短連接與長連接
短連接是指 socket 建立連線並發送完資料、接收完畢後,即馬上中斷連線;而長連線則是 socket 建立連線後即保持連接。
在 HTTP 1.0 中所有的檔案請求預設都是個別的短連接,而在 HTTP 1.1 後預設為長連接,即 Connection: keep-alive
,因此建立連線後不會中斷,但這有一個維持的時間區段,超過時間區段後還是會中斷連線。
HTTP/2
HTTP/2 是 HTTP 的下一代規範,主要差異是
- 在 HTTP 1.1 中,每個資源的請求都需使用單獨的 network thread 建立一個 TCP/IP 連線,但對瀏覽器來說同一時間每個網域所能發出的請求數是有限的,因此若請求資源過多就需等待先前的資源取得完畢,才能再發出請求,因此下載速度緩慢。
- 在 HTTP/2 中,每個 TCP/IP 連線可請求多個資源,意即將資源分割成更小的 frame 來做請求和交錯傳輸,傳輸速度顯著提升。
並且由於請求與回應標頭的壓縮、二進制封包結構、伺服器端推送和請求優先順序的排定,因此 HTTP/2 能提供更好的效能,可參考這裡。
HTTPS
HTTPS 即是更安全的 HTTP,白話說就是建立 SSL 連線,以確保之後的通訊都是加密的,無法被輕易擷取分析。由於 HTTPS 比起 HTTP 需要額外建立安全連線和加密而需要傳輸更多資料(速度慢),因此搭配 HTTP/2 會讓速度加快。
四、HTTP 快取
快取規則
- ETag:用來驗證檔案是否更新,若檔案未更新,則不回傳檔案。
- Cache-control:每個資源皆可設定 Response Headers 的 Cache-control 來定義快取策略。
- no-cache, no-store:主要用於決定是否要求更新檔案,no-cache 表示要求伺服器檔案時必須以 ETag 確認是否有更新檔案,若無更新,伺服器不需回應檔案;no-store 表示禁止儲存快取,每次都必須跟伺服器要求新檔案,適用於私人或機密資料。
- public, private, must-revalidate :可快取回應的使用者,public 表示可供所有中繼設備和瀏覽器快取,例如 ISP Cache、CDN 快取和瀏覽器快取;private 只限單一使用者快取,例如可供瀏覽器快取;must-revalidate 表示表示可使用快取檔案,但過期時必定要去伺服器驗證是否有新檔案。
- max-age:快取時限,以秒為單位,
max-age: 600
表示快取時限為 10 分鐘,10 分鐘後這個快取檔案就過期了,必須跟伺服器要求更新後的檔案。max-age 覆寫 expires header 的設定,讓 expires header 作為 fallback 機制。
範例如下,檔案可供所有設備快取,過期時間是兩年後。
如何決定要使用快取還是取得更新資源?
流程如下圖。
如何強迫瀏覽器更新資源?
方法一
變更資源的網址,例如加入 hash。相同網址之下,會讀取本地快取,所以改變網址即可,可解決資源未過期但想要更新的狀況。
以下三個網址對瀏覽器來說都是不一樣的,不會讀取本地快取,會重新要求資源。
https://www.sample.com.tw/assets/cute.png
https://www.sample.com.tw/assets/cute.png?123
https://www.sample.com.tw/assets/cute.v123.png?123
方法二
使用 ctrl + F5
或 ctrl
並點擊瀏覽器的更新按鈕來強制更新,重新向伺服器請求所有資源,而不使用快取,因此可取得最新版本的內容。
Checklist
- 統一網址格式,避免同一資源因不同網址而多次取得或重複快取此資源內容。
- 在伺服器端為資源設定 ETag,讓資源未更新時不需回應。
- 決定資源可快取的使用者,例如 public 或 private,是否可被中繼設備快取?私密資訊只允許讓私人快取?
- 決定資源快取期限。
- 決定快取條件,例如 no-cache、no-store 或加入 hash 改變資源網址以強迫更新資源。
- 確定資源經過最佳化,以減少更新資源的成本。
範例
資源未被快取。
資源來源是記憶體快取。
資源來源是 HTTP 快取(HTTP Cache)又稱為磁碟快取(Disk Cache)。
資源來源是記憶體快取,但使用 Hash 控制是否要強迫瀏覽器更新資源。
資源經 ETag 確認未更新,因此使用快取檔案。
備註
五、瀏覽器解析與渲染頁面
客戶端(意即瀏覽器的 renderer process)收到從伺服器的回應後後會開始做解析和渲染,這部份的流程為
- 解析 HTML 以建立 DOM tree。
- 解析 CSS 以建立 CSSOM(註 4)。
- 結合 DOM tree 與 CSSOM 產生 render tree、字型處理(註 5)。
- 利用 render tree 計算可視元素的版面配置、繪製和合成,這部份可能會有些 JavaScript 程式碼導致重新佈局或重繪,瀏覽器完成渲染即可將畫面呈現給使用者。
這部份的優化包含
- 避免不必要或大量的 DOM 操作,可以利用 fragment 來做批次處理。
- 樣式的改變儘量更動合成階段就好。
- 使用 requestAnimationFrame 讓瀏覽器依照自身狀況優化效能、不中斷目前進行的工作。
- 使用
will-change
升階,減少繪製區域,但不可過度使用,以免增加合成階段的負擔。 - 複雜運算丟到 worker thread 即可不阻塞 main thread,避免阻礙渲染以發生顫動(jank)。
- 避免改變字體大小,這會造成重新計算版面配置的問題。
備註
- 註 4:CSSOM(CSS Object Model)是指由頁面節點與樣式對應而產生的樹狀結構,與 DOM 類似,在 CSSOM 尚未建立前,雖然不會阻礙建構 DOM tree,但會阻塞渲染(注意,Media Query 的部份不會阻礙渲染),因此畫面不會有任何東西(白屏)。即使 CSS 文件被暫存起來,但每次頁面的載入都須重新建立 CSSOM,因此,撰寫高效能的樣式(例如:優化關鍵轉譯路徑)與避免 JavaScript 的阻塞是必要的。
- 註 5:在 render tree 指定所需字體後會來請求字型檔,如果請求的字型檔尚未下載完畢,不同瀏覽器有不同的解法,例如:Chrome 和 Firefox 會等待三秒鐘,若超過三秒鐘字體檔仍未下載完畢就會啟用備用字體,待下載完畢再重新渲染字型,可參考這裡。
阻礙渲染的原因和解法
阻礙渲染的原因有
- CSS 檔案的載入與建立 CSSOM。
- 當在 HTML 遇到
<script>
標籤時,會阻礙 HTML 的剖析,立刻進行 JavaScript 檔案的下載、解析和執行。
解法為
- 儘早建立 CSSOM。
- 樣式要放在
<head>
儘早執行,或使用 Media Query 避免非必要的檔案下載所導致的阻塞。 - 將第一頻必須的樣式直接用
<style>
嵌入,避免使用外部檔案而等待傳輸。
- 樣式要放在
- 不要使用 CSS
@import
(而要用<link>
引入),這個指令會使得剖析器必須先下載並剖析當前的檔案後才會知道有其他檔案而再去下載這些被 import 的檔案,因此讓 CSS 檔案無法被平行下載,減慢畫面生成的速度。 - 避免
<script>
阻礙 HTML parser 的進行。<script>
要放在<body>
結束之前,避免阻礙畫面顯示。<script>
加上屬性 async 或 defer。- async 在背景下載檔案,下載完後會暫停 HTML parser 來執行 JavaScript 程式碼,待執行完成後再恢復 HTML parser。
- defer 同樣也是在背景下載檔案,但下載完成後不會馬上執行,而是待 HTML parser 剖析完畢 HTML 後才執行 JavaScript 程式碼。
圖片來源:Loading Third-Party JavaScript
可參考-瀏覽器渲染效能和 Renderer Process 做了什麼事情?
Web Font
如前所述,在 render tree 指定所需字體後會來請求字型檔,如果請求的字型檔尚未下載完畢,不同瀏覽器有不同的解法,例如:Chrome 和 Firefox 會等待三秒鐘(在這段時間會沒有文字,可能會是白屏喔),若超過三秒鐘字體檔仍未下載完畢就會啟用備用字體,待下載完畢再重新渲染字型;IE 則是無等待時間,馬上啟用啟用備用字體,待下載完畢再重新渲染字型;Safari 則是無限制時間的等待下載(30 秒以上),可參考這裡。
關於 web font 的優化方式有…
- 避免使用過多的字體或變體數量,以減少下載的檔案大小。
- 若需符合特殊字型需求,可將字體檔可拆分成許多子集檔案或使用
unicode-range
指定子集範圍,這樣就能減小檔案大小並提高下載速度。 - 使用 GZIP 壓縮字體檔以減少檔案大小。
- 使用
<link rel="preload">
、font-display
或 Font Loading API 來自行定義字體的下載和呈現(依照下載時間決定是否渲染或使用 fallback),這是因為預設的延遲下載可能導致頁面延遲呈現。 - 在
src
中設定local(...)
來讀取本機檔案,避免發出不必要的 HTTP 請求。 - 使用 HTTP Cacheing 並設定快取策略,由於字體檔通常不常修改,使用快取能避免浪費 HTTP 請求。
JavaScript
除了前面提到的 JavaScript 檔案下載或執行時可能會阻礙建構 DOM tree 而可利用 async 或 defer 來解決之外(可參考這裡),繼續討論更多關於 JavaScript 的議題。
網頁的生命週期
以「事件」來區分網頁的生命週期的話,大致可區分為幾個部份-loaded、DOMContentLoaded、beforeunload、unload。
- DOMContentLoaded:這個階段就是我們常聽到的「DOM ready」,意即瀏覽器已將 DOM tree 與 CSSOM 建構完畢,但
<img>
等外部資源可能尚未下載完畢(註 3),功能幾乎等同於 jQuery 的jQuery.ready()
。 - load:瀏覽器已將所有資源下載完畢。
- beforeunload:網頁被卸載時觸發,也可說是離開這個頁面時觸發,例如:關閉網頁、往上下頁、前往其他頁、重新整理頁面等。
- unload:網頁被下載後觸發,也可說是離開這個網頁後觸發。
這些事件的用途是…
- DOMContentLoaded:存取 DOM 的內容。
- load:存取圖片。
- beforeunload:試圖離開此頁面時跳出警示。
- unload:離開此頁後的處理。
推薦閱讀-重新認識 JavaScript 番外篇 (6) - 網頁的生命週期。
備註
- 註 3:圖檔是非同步下載,所以不會阻礙解析或渲染,下載完畢就會用圖檔替換 src 屬性的部份。
Service Worker
service worker(在此簡稱 sw)可將資料快取在本地,並主動掌握檔案更新的時機。browser process 的 ui thread 在發出網路請求並請 network process 處理後續工作時,network thread 會先到已註冊的 sw scope 確認是否有想要的檔案,若有就將這個檔案交由 renderer process 來處理。另外,「navigation preload」是一種讓 sw 平行下載其所需資料的加速機制,在 sw 被啟動的同時,network thread 會發出網路請求來下載 sw 所要求的資源,而非待 sw 要求後才下載檔案。
編譯與執行
在 JavaScript 引擎編譯和解析 JavaScript 程式碼時會經過三個步驟-(1) 語法基本單元化與語彙分析、(2) 剖析(或稱語法分析)和 (3) 產生目的程式碼,最後執行編譯後的指令…因而帶出以下觀念…
編譯階段
- 補分號:JavaScript 引擎中的剖析器(parser)會自動幫程式碼補上分號(Automatic Semicolon Insertion,ASI),以避免剖析失敗。
- 範疇:編譯器或 JavaScript 引擎藉由識別字名稱(identifier name)查找變數的一組規則。
- 閉包:函式記得並存取語彙範疇的能力,可說是指向特定範疇的參考,因此當函式是在其宣告的語彙範疇之外執行時也能正常運作。
- 變數或函式提升(hoisting):編譯器在編譯時期會先找出所有的變數並綁定所屬範疇,但不賦值,所以此刻變數所帶的值是 undefined;而在執行階段,JavaScript 引擎才會處理給值的事情。可以想成是把這些變數和函示「提升」到程式碼的最頂端,這就是所謂的拉升(hoisting)。另外,也會看到 ES6 的 let、const、TDZ 等觀念。
執行階段
- VO:函式內的執行環境(execution context)所對應到的 variable object(簡稱 VO),可想像成是一個物件,儲存程式碼執行到此刻函式和變數的值。
- this:function 執行時所屬的物件,this 是在執行時期做綁定,其值和函式在哪裡被呼叫有關。
- 判斷 this 的值的四個規則,並以匹配的優先順序由高至低排列
- new 綁定:this 會指向 new 出來的物件。
- 明確綁定:使用 call、apply、bind,明確指出要綁定給 this 的物件。
- 隱含綁定:當函式為物件的方法時,在執行階段 this 就會被綁定至該物件。
- 預設綁定:當以上規則都不適用時,就套用預設綁定,在非嚴格模式下,瀏覽器環境 this 的值是預設值全域物件 window,而在嚴格模式下,this 的值是 undefined。
- 箭頭函數(arrow function)的 this 的值並不適用上面提到的四種規則,this 強制綁定為執行環境,例如在瀏覽器中就是 window。
- 判斷 this 的值的四個規則,並以匹配的優先順序由高至低排列
- 原型串鏈、行為委派。
- 垃圾回收:使用區塊範疇
{..}
解決因閉包而保留的變數所配置的記憶體空間問題,點此看範例。
JavaScript 全系列筆記可參考這裡。
六、其它
跨域
什麼是跨域?
「同源政策」(Same Origin Policy)讓瀏覽器不能對不同網域的資源發出網路請求,目的是避免惡意網站竊取資料,其中,同源政策又分為兩種「DOM 的同源政策」與「cookie 的同源政策」。
對於 DOM 的同源政策來說,只要協定類型、伺服器位址與埠號相同即是同源,而且並非所有跨網域的行為都會被禁止,例如:連結、重新導向或表單送出、跨來源嵌入資源等是允許的,但透過 JavaScript 的跨來源寫入是不行的,例如:XMLHttpRequest 或 Fetch API 的跨來源寫入或讀取。也就是說,同源政策允許 HTML 標籤的跨來源寫入、嵌入、讀取,但對於 JavaScript 產生的跨來源寫入、嵌入、讀取是有限制的。也因為對於 HTML 標籤的跨來源存取是寬鬆的,因此可能會有 CSRF 問題。
對於 cookie 的同源政策來說,卻可透過設定允許子網域的程式碼經由 JavaScript 修改母網域的 cookie,例如:伺服器端設定 Set-Cookie: key=value; domain=.sample.com; path=/
或客戶端設定 document.domain = 'sample.com';
,這樣不論是 a.sample.com 或 sample.com 可都取用和修改。
跨域的解法?
JSONP
JSONP 的解法主要是利用 <script>
具有跨域能力,透過在客戶端網頁加入 <script>
標籤,並且伺服器端在用函式包裹資料後回傳。注意,這只能是 GET 請求而不能是 POST,如果是 POST 就要用 CORS。
範例如下。
客戶端。
function appendScriptTag(src) {
var script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.src = src;
document.body.appendChild(script);
}
window.onload = function () {
appendScriptTag('http://sample.com?cb=foo');
};
function foo(data) {
console.log('response data: ' + JSON.stringify(data));
}
伺服器端。
foo({ testData: 'Hello World' });
CORS
跨域資源共享(Cross-origin Resource Sharing,CORS)允許瀏覽器跨網域向伺服器發出請求,這主要是在後端對 header 進行設定 Access-Control-Allow-Origin
、Access-Control-Allow-Headers
與 Access-Control-Allow-Methods
。
優化
預請求是指在發出真正的請求前,會將一些資訊丟到目標伺服器詢問是否合法,意即使用請求方法 OPTIONS 表示這個請求是用來詢問的,在標頭中放入 Origin 表示來自哪裡的請求,還有 Access-Control-Request-Method 和 Access-Control-Request-Headers,若目標伺服器確認允許就會回傳 200 OK,這時候客戶端會再發出真正的請求,才會包含資料。因此,在標頭設定 Access-Control-Max-Age
可暫存此次預請求回應的快取秒數,在時間範圍內的預請求都會使用這個回應而非再次提出預請求的要求。
iframe postMessage / message
範例如下,父窗口 http://parent.com
向子窗口 http://child.com
使用 postMessage 發送訊息。
var popup = window.open('http://child.com', 'title');
popup.postMessage('Hello World!', 'http://child.com');
子窗口向父窗口使用 postMessage 發送訊息。
window.opener.postMessage('Hello World, too!', 'http://parent.com');
父窗口或子窗口都透過 message 可監聽對方的訊息。
window.addEventListener(
'message',
function (e) {
console.log(e.source); // 發送訊息的窗口
console.log(e.origin); // 訊息發送的目標網址
console.log(e.data); // 訊息內容
},
false,
);
WebSocket
WebSocket 透過 origin 表示源自哪個網域,由接受到的伺服器判斷是否合法。
viewport
什麼是 viewport ?
viewport 是指網頁的可視區域,即視窗的寬度。而 meta viewport 的設定可讓畫面在行動裝置上能正常顯示,或響應式網頁可根據 viewport 與 DPR 選擇適合的圖檔。
像素與螢幕像素
像素(px)是指螢幕上所顯示的最小單位,可看作是一個點,但這個點會依據 DPR 的不同而可大可小;而螢幕像素密度(Device Pixel Ratio,DPR)是指每一英吋裡到底含有多少像素。一般來說,安卓的 DPR 多為 1,而蘋果的手機是 2,平板電腦是 3,因此在安卓上看起來很正常的圖片,往往在 iPhone 等蘋果產品上都會糊糊的( <picture>/<img srcset sizes>
詳解與範例)。
<meta name=”viewport”>
的設定與說明
- width:用途是設定視窗的寬度,預設值為 980px,最小值為 200px,最大值為 10000px,合法值為 device-width 或正整數。
- height:用途是設定視窗的高度,預設值為螢幕的長寬比(aspect ratio),最小值為 223px,最大值為 10000px,合法值為 device-height 或正整數。
- initial-scale:用途是設定畫面的初始縮放比例,預設值為螢幕寬度,最小值為 0.1,最大值為 10.0,合法值為浮點數。
- minimum-scale:用途是設定畫面的最小縮放比例,預設值為 0.25,最小值為 0.1,最大值為 10.0,合法值為小於 maximum-scale 的浮點數。
- maximum-scale:用途是設定畫面的最大縮放比例,預設值為 1.6,最小值為 0.1,最大值為 10.0,合法值為大於 minimum-scale 的浮點數。
- user-scalable:用途是設定是否允許使用者改變縮放比例,預設值為 yes,合法值為 yes 或 no。
回顧
梳理知識體系可內省不足而能更深入了解…
…
回顧本文的重點
- 瀏覽器的內部運作機制:多程序與多執行緒的架構、網址解析、如何減少網路請求數目的方法。
- 網路連線:DNS 解析與其優化方案-DNS 快取、DNS 負載平衡與 DNS Prefetch;建立 TCP/IP 連線並比較 TCP/IP 與 OSI 兩種網路模型架構的異同。
- 伺服器處理請求並返回 HTTP 回應:HTTP 的請求和回應訊息的結構、狀態碼的意義、不同平台的 CRLF 的表示方式、各種加快網路傳輸效能的優化方案(針對 cookie 的域名拆分、Gzip、長短連接、HTTP/2 與 HTTPS)。
- HTTP 快取:快取規則(ETag、Cache-control)、瀏覽器如何決定要使用快取還是取得更新資源、如何強迫瀏覽器更新資源。
- 瀏覽器解析與渲染頁面:解析與渲染流程、阻礙渲染的原因和解法、網頁的生命週期、JavaScript 的編譯與執行和重要觀念。
- 其它:跨域、viewport。
參考資料
- 從輸入 URL 到頁面加載完成的過程中都發生了什麼事情?
- 從輸入 URL 到頁面加載的過程?如何由一道題完善自己的前端知識體系!
- Responsive Web Design 基礎 : <meta name=”viewport” > 設定
- Same Origin Policy 同源政策 ! 一切安全的基礎
- 我知道你懂 hoisting,可是你了解到多深?
- 重新認識 JavaScript 番外篇 (6) - 網頁的生命週期
- 你的網站升級到 HTTP/2 了嗎?
- Git 在 Windows 平台處理斷行字元 (CRLF) 的注意事項
- 什麼是 OSI 的 7 層架構?和常聽到的 Layer 7 有關?
- Web Font Optimization
- Controlling Font Performance with font-display
- How we use web fonts responsibly, or, avoiding a
@font-face-palm
- 跨域資源共享 CORS 詳解
- 輕鬆理解 Ajax 與跨來源請求
- 瀏覽器同源政策及其規避方法
- AJAX 請求真的不安全麼?談談 Web 安全與 AJAX 的關係。