從Chrome源碼看瀏覽器如何加載資源
對(duì)瀏覽器加載資源有很多不確定性,例如:
- css/font的資源的優(yōu)化級(jí)會(huì)比img高,資源的優(yōu)化級(jí)是怎么確定的呢?
- 資源優(yōu)先級(jí)又是如何影響加載的先后順序的?
- 有幾種情況可能會(huì)導(dǎo)致資源被阻止加載?
通過源碼可以找到答案。此次源碼解讀基于Chromium 64(10月28日更新的源碼)。
下面通過加載資源的步驟,依次說明。
1. 開始加載
通過以下命令打開Chromium,同時(shí)打開一個(gè)網(wǎng)頁:
- chromium --renderer-startup-dialog https://www.baidu.com
Chrome會(huì)在DocumentLoader.cpp里面通過以下代碼去加載:
- enum Type : uint8_t {
- kMainResource,
- kImage,
- kCSSStyleSheet,
- kScript,
- kFont,
- kRaw,
- kSVGDocument,
- kXSLStyleSheet,
- kLinkPrefetch,
- kTextTrack,
- kImportResource,
- kMedia, // Audio or video file requested by a HTML5 media element
- kManifest,
- kMock // Only for testing
- };
除了常見的image/css/js/font之外,我們發(fā)現(xiàn)還有像textTrack的資源,這個(gè)是什么東西呢?這個(gè)是video的字幕,使用webvtt格式:
- <video controls poster="/images/sample.gif">
- <source src="sample.mp4" type="video/mp4">
- <track kind="captions" src="sampleCaptions.vtt" srclang="en">
- </video>
還有動(dòng)態(tài)請(qǐng)求ajax屬于Raw類型。因?yàn)閍jax可以請(qǐng)求多種資源。
MainResource包括location即導(dǎo)航輸入地址得到的頁面、使用frame/iframe嵌套的、通過超鏈接點(diǎn)擊的頁面以及表單提交這幾種。
接著交給稍底層的ResourceFecher去加載,所有資源都是通過它加載:
- fetcher->RequestResource(
- params, RawResourceFactory(Resource::kMainResource), substitute_data)
在這個(gè)里面會(huì)先對(duì)請(qǐng)求做預(yù)處理。
2. 預(yù)處理請(qǐng)求
每發(fā)個(gè)請(qǐng)求會(huì)生成一個(gè)ResourceRequest對(duì)象,這個(gè)對(duì)象包含了http請(qǐng)求的所有信息:
包括url、http header、http body等,還有請(qǐng)求的優(yōu)先級(jí)信息等:
然后會(huì)根據(jù)頁面的加載策略對(duì)這個(gè)請(qǐng)求做一些預(yù)處理,如下代碼:
- PrepareRequestResult result = PrepareRequest(params, factory, substitute_data,
- identifier, blocked_reason);
- if (result == kAbort)
- return nullptr;
- if (result == kBlock)
- return ResourceForBlockedRequest(params, factory, blocked_reason);
prepareRequest會(huì)做兩件事情,一件是檢查請(qǐng)求是否合法,第二件是把請(qǐng)求做些修改。如果檢查合法性返回kAbort或者kBlock,說明資源被廢棄了或者被阻止了,就不去加載了。
被block的原因可能有以下幾種:
- enum class ResourceRequestBlockedReason {
- kCSP, // CSP內(nèi)容安全策略檢查
- kMixedContent, // mixed content
- kOrigin, // secure origin
- kInspector, // devtools的檢查器
- kSubresourceFilter,
- kOther,
- kNone
- };
源碼里面會(huì)在這個(gè)函數(shù)做合法性檢查:
- blocked_reason = Context().CanRequest(/*參數(shù)省略*/);
- if (blocked_reason != ResourceRequestBlockedReason::kNone) {
- return kBlock;
- }
CanRequest函數(shù)會(huì)相應(yīng)地檢查以下內(nèi)容:
(1)CSP(Content Security Policy)內(nèi)容安全策略檢查
CSP是減少XSS攻擊一個(gè)策略。如果我們只允許加載自己域的圖片的話,可以加上下面這個(gè)meta標(biāo)簽:
- <meta http-equiv="Content-Security-Policy" content="img-src 'self';">
或者是后端設(shè)置這個(gè)http響應(yīng)頭。
self表示本域,如果加載其它域的圖片瀏覽器將會(huì)報(bào)錯(cuò):
所以這個(gè)可以防止一些XSS注入的跨域請(qǐng)求。
源碼里面會(huì)檢查該請(qǐng)求是否符合CSP的設(shè)定要求:
- const ContentSecurityPolicy* csp = GetContentSecurityPolicy();
- if (csp && !csp->AllowRequest(
- request_context, url, options.content_security_policy_nonce,
- options.integrity_metadata, options.parser_disposition,
- redirect_status, reporting_policy, check_header_type)) {
- return ResourceRequestBlockedReason::kCSP;
- }
如果有CSP并且AllowRequest沒有通過的話就會(huì)返回堵塞的原因。具體的檢查過程是根據(jù)不同的資源類型去獲取該類資源資源的CSP設(shè)定進(jìn)行比較。
接著會(huì)根據(jù)CSP的要求改變請(qǐng)求:
- ModifyRequestForCSP(request);
主要是升級(jí)http為https。
(2)upgrade-insecure-requests
如果設(shè)定了以下CSP規(guī)則:
- <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
那么會(huì)將網(wǎng)頁的http請(qǐng)求強(qiáng)制升級(jí)為https,這是通過改變r(jià)equest對(duì)象實(shí)現(xiàn)的:
- url.SetProtocol("https");
- if (url.Port() == 80)
- url.SetPort(443);
- resource_request.SetURL(url);
包括改變url的協(xié)議和端口號(hào)。
(3)Mixed Content混合內(nèi)容block
在https的網(wǎng)站請(qǐng)求http的內(nèi)容就是Mixed Content,例如加載一個(gè)http的JS腳本,這種請(qǐng)求通常會(huì)被瀏覽器堵塞掉,因?yàn)閔ttp是沒有加密的,容易受到中間人的攻擊,如修改JS的內(nèi)容,從而控制整個(gè)https的頁面,而圖片之類的資源即使內(nèi)容被修改可能只是展示出問題,所以默認(rèn)沒有block掉。源碼里面會(huì)檢查Mixed Content的內(nèi)容:
- if (ShouldBlockFetchByMixedContentCheck(request_context, frame_type,
- resource_request.GetRedirectStatus(),
- url, reporting_policy))
- return ResourceRequestBlockedReason::kMixedContent;
在源碼里面,以下4種資源是optionally-blockable(被動(dòng)混合內(nèi)容):
- // "Optionally-blockable" mixed content
- case WebURLRequest::kRequestContextAudio:
- case WebURLRequest::kRequestContextFavicon:
- case WebURLRequest::kRequestContextImage:
- case WebURLRequest::kRequestContextVideo:
- return WebMixedContentContextType::kOptionallyBlockable;
什么叫被動(dòng)混合內(nèi)容呢?W3C文檔是這么說的:那些不會(huì)打破頁面重要部分,風(fēng)險(xiǎn)比較低的,但是使用頻率又比較高的Mixed Content內(nèi)容。
而剩下的其它所有資源幾乎都是blockable的,包括JS/CSS/Iframe/XMLHttpRequest等:
我們注意到img srcset里的資源也是默認(rèn)會(huì)被阻止的,即下面的img會(huì)被block:
- <img srcset="http://fedren.com/test-1x.png 1x, http://fedren.com/test-2x.png 2x" alt>
但是使用src的不會(huì)被block:
- <img src="http://fedren.com/images/sell/icon-home.png" alt>
如下圖所示:
這就是optionally-blockable和blocakable資源的區(qū)分。
對(duì)于被動(dòng)混合內(nèi)容,如果設(shè)置strick mode:
- <meta http-equiv="Content-Security-Policy" content="block-all-mixed-content">
那么即使是optionally的也會(huì)被block掉:
- case WebMixedContentContextType::kOptionallyBlockable:
- allowed = !strict_mode;
- if (allowed) {
- content_settings_client->PassiveInsecureContentFound(url);
- client->DidDisplayInsecureContent();
- }
- break;
上面代碼,如果strick_mode是true,allowed就是false,被動(dòng)混合內(nèi)容就會(huì)被阻止。
而對(duì)于主動(dòng)混合內(nèi)容,如果用戶設(shè)置允許加載:
那么也是可以加載的:
- case WebMixedContentContextType::kBlockable: {
- // Strictly block subresources that are mixed with respect to their
- // subframes, unless all insecure content is allowed. This is to avoid the
- // following situation: https://a.com embeds https://b.com, which loads a
- // script over insecure HTTP. The user opts to allow the insecure content,
- // thinking that they are allowing an insecure script to run on
- // https://a.com and not realizing that they are in fact allowing an
- // insecure script on https://b.com.
- bool should_ask_embedder =
- !strict_mode && settings &&
- (!settings->GetStrictlyBlockBlockableMixedContent() ||
- settings->GetAllowRunningOfInsecureContent());
- allowed = should_ask_embedder &&
- content_settings_client->AllowRunningInsecureContent(
- settings && settings->GetAllowRunningOfInsecureContent(),
- security_origin, url);
- break;
代碼倒數(shù)第4行會(huì)去判斷當(dāng)前的client即當(dāng)前頁面的設(shè)置是否允許加載blockable的資源。另外源碼注釋還提到了一種特殊的情況,就是a.com的頁面包含了b.com的頁面,b.com允許加載blockable的資源,a.com在非strick mode的時(shí)候頁面是允許加載的,但是如果a.com是strick mode,那么將不允許加載。
并且如果頁面設(shè)置了strick mode,用戶設(shè)置的允許blockable資源加載的設(shè)置將會(huì)失效:
- // If we're in strict mode, we'll automagically fail everything, and
- // intentionally skip the client checks in order to prevent degrading the
- // site's security UI.
- bool strict_mode =
- mixed_frame->GetSecurityContext()->GetInsecureRequestPolicy() &
- kBlockAllMixedContent ||
- settings->GetStrictMixedContentChecking();
這個(gè)主要是svg使用use的獲取svg資源的時(shí)候必須不能跨域,如下以下資源將會(huì)被阻塞:
- <svg>
- <use href="http://cdn.test.com/images/logo.svg#abc"></use>
- </svg>
如下圖所示:
并且控制臺(tái)會(huì)打印:
源碼里面會(huì)對(duì)這種use link加載的svg做一個(gè)檢驗(yàn):
- case Resource::kSVGDocument:
- if (!security_origin->CanRequest(url)) {
- PrintAccessDeniedMessage(url);
- return ResourceRequestBlockedReason::kOrigin;
- }
- break;
具體檢驗(yàn)CanRequest函數(shù)主要是檢查是否同源:
- // We call isSameSchemeHostPort here instead of canAccess because we want
- // to ignore document.domain effects.
- if (IsSameSchemeHostPort(target_origin.get()))
- return true;
- return false;
如果協(xié)議、域名、端口號(hào)都一樣則通過檢查。需要這里和同源策略是兩碼事,這里的源阻塞是連請(qǐng)求都發(fā)不出去,而同源策略只是阻塞請(qǐng)求的返回結(jié)果。
svg的use外鏈一般是用來做svg的雪碧圖的,但是為什么需要同源呢,如果不同源會(huì)有什么不安全的因素?這里我也不清楚,暫時(shí)沒查到,W3C只是說明了需要同源,但沒有給出原因。
以上就是3種主要的block的原因。在預(yù)處理請(qǐng)求里面除了判斷資源有沒有被block或者abort(abort的原因通常是url不合法),還會(huì)計(jì)算資源的加載優(yōu)先級(jí)。
3. 資源優(yōu)先級(jí)
(1)計(jì)算資源加載優(yōu)先級(jí)
通過調(diào)用以下函數(shù)設(shè)定:
- resource_request.SetPriority(ComputeLoadPriority(
- resource_type, params.GetResourceRequest(), ResourcePriority::kNotVisible,
- params.Defer(), params.GetSpeculativePreloadType(),
- params.IsLinkPreload()));
我們來看一下這個(gè)函數(shù)里面是怎么計(jì)算當(dāng)前資源的優(yōu)先級(jí)的。
首先每個(gè)資源都有一個(gè)默認(rèn)的優(yōu)先級(jí),這個(gè)優(yōu)先級(jí)做為初始化值:
- ResourceLoadPriority priority = TypeToPriority(type);
不同類型的資源優(yōu)先級(jí)是這么定義的:
- ResourceLoadPriority TypeToPriority(Resource::Type type) {
- switch (type) {
- case Resource::kMainResource:
- case Resource::kCSSStyleSheet:
- case Resource::kFont:
- // Also parser-blocking scripts (set explicitly in loadPriority)
- return kResourceLoadPriorityVeryHigh;
- case Resource::kXSLStyleSheet:
- DCHECK(RuntimeEnabledFeatures::XSLTEnabled());
- case Resource::kRaw:
- case Resource::kImportResource:
- case Resource::kScript:
- // Also visible resources/images (set explicitly in loadPriority)
- return kResourceLoadPriorityHigh;
- case Resource::kManifest:
- case Resource::kMock:
- // Also late-body scripts discovered by the preload scanner (set
- // explicitly in loadPriority)
- return kResourceLoadPriorityMedium;
- case Resource::kImage:
- case Resource::kTextTrack:
- case Resource::kMedia:
- case Resource::kSVGDocument:
- // Also async scripts (set explicitly in loadPriority)
- return kResourceLoadPriorityLow;
- case Resource::kLinkPrefetch:
- return kResourceLoadPriorityVeryLow;
- }
- return kResourceLoadPriorityUnresolved;
- }
可以看到優(yōu)先級(jí)總共分為五級(jí):very-high、high、medium、low、very-low,其中MainRescource頁面、CSS、字體這三個(gè)的優(yōu)先級(jí)是最高的,然后就是Script、Ajax這種,而圖片、音視頻的默認(rèn)優(yōu)先級(jí)是比較低的,最低的是prefetch預(yù)加載的資源。
什么是prefetch的資源呢?有時(shí)候你可能需要讓一些資源先加載好等著用,例如用戶輸入出錯(cuò)的時(shí)候在輸入框右邊顯示一個(gè)X的圖片,如果等要顯示的時(shí)候再去加載就會(huì)有延時(shí),這個(gè)時(shí)候可以用一個(gè)link標(biāo)簽:
- <link rel="prefetch" href="image.png">
瀏覽器空閑的時(shí)候就會(huì)去加載。另外還可以預(yù)解析DNS:
- <link rel="dns-prefetch" href="https://cdn.test.com">
預(yù)建立TCP連接:
- <link rel="preconnect" href="https://cdn.chime.me">
后面這兩個(gè)不屬于加載資源,這里順便提一下。
注意上面的switch-case設(shè)定資源優(yōu)先級(jí)有一個(gè)順序,如果既是script又是prefetch的話得到的優(yōu)化級(jí)是high,而不是prefetch的very low,因?yàn)閜refetch是最后一個(gè)判斷。所以在設(shè)定了資源默認(rèn)的優(yōu)先級(jí)之后,會(huì)再對(duì)一些情況做一些調(diào)整,主要是對(duì)prefetch/preload的資源。包括:
a)降低preload的字體的優(yōu)先級(jí)
如下代碼:
- // A preloaded font should not take precedence over critical CSS or
- // parser-blocking scripts.
- if (type == Resource::kFont && is_link_preload)
- priority = kResourceLoadPriorityHigh;
會(huì)把預(yù)加載字體的優(yōu)先級(jí)從very-high變成high
b)降低defer/async的script的優(yōu)先級(jí)
如下代碼:
- if (type == Resource::kScript) {
- // Async/Defer: Low Priority (applies to both preload and parser-inserted)
- if (FetchParameters::kLazyLoad == defer_option) {
- priority = kResourceLoadPriorityLow;
- }
- }
script如果是defer的話,那么它優(yōu)先級(jí)會(huì)變成最低。
4)頁面底部preload的script優(yōu)先級(jí)變成medium
如下代碼:
- if (type == Resource::kScript) {
- // Special handling for scripts.
- // Default/Parser-Blocking/Preload early in document: High (set in
- // typeToPriority)
- // Async/Defer: Low Priority (applies to both preload and parser-inserted)
- // Preload late in document: Medium
- if (FetchParameters::kLazyLoad == defer_option) {
- priority = kResourceLoadPriorityLow;
- } else if (speculative_preload_type ==
- FetchParameters::SpeculativePreloadType::kInDocument &&
- image_fetched_) {
- // Speculative preload is used as a signal for scripts at the bottom of
- // the document.
- priority = kResourceLoadPriorityMedium;
- }
- }
如果是defer的script那么優(yōu)先級(jí)調(diào)成最低(上面第3小點(diǎn)),否則如果是preload的script,并且如果頁面已經(jīng)加載了一張圖片就認(rèn)為這個(gè)script是在頁面偏底部的位置,就把它的優(yōu)先級(jí)調(diào)成medium。通過一個(gè)flag決定是否已經(jīng)加載過第一張圖片了:
- // Resources before the first image are considered "early" in the document and
- // resources after the first image are "late" in the document. Important to
- // note that this is based on when the preload scanner discovers a resource
- // for the most part so the main parser may not have reached the image element
- // yet.
- if (type == Resource::kImage && !is_link_preload)
- image_fetched_ = true;
資源在第一張非preload的圖片前認(rèn)為是early,而在后面認(rèn)為是late,late的script的優(yōu)先級(jí)會(huì)偏低。
什么叫preload呢?preload不同于prefetch的,在早期瀏覽器,script資源是阻塞加載的,當(dāng)頁面遇到一個(gè)script,那么要等這個(gè)script下載和執(zhí)行完了,才會(huì)繼續(xù)解析剩下的DOM結(jié)構(gòu),也就是說script是串行加載的,并且會(huì)堵塞頁面其它資源的加載,這樣會(huì)導(dǎo)致頁面整體的加載速度很慢,所以早在2008年的時(shí)候?yàn)g覽器出了一個(gè)推測(cè)加載(speculative preload)策略,即遇到script的時(shí)候,DOM會(huì)停止構(gòu)建,但是會(huì)繼續(xù)去搜索頁面需要加載的資源,如看下后續(xù)的html有沒有img/script標(biāo)簽,先進(jìn)行預(yù)加載,而不用等到構(gòu)建DOM的時(shí)候才去加載。這樣大大提高了頁面整體的加載速度。
5)把同步即堵塞加載的資源的優(yōu)先級(jí)調(diào)成最高
如下代碼:
- // A manually set priority acts as a floor. This is used to ensure that
- // synchronous requests are always given the highest possible priority
- return std::max(priority, resource_request.Priority());
如果是同步加載的資源,那么它的request對(duì)象里面的優(yōu)先最級(jí)是最高的,所以本來是hight的ajax同步請(qǐng)求在最后return的時(shí)候會(huì)變成very-high。
這里是取了兩個(gè)值的最大值,第一個(gè)值是上面進(jìn)行各種判斷得到的priority,第二個(gè)在初始這個(gè)ResourceRequest對(duì)象本身就有的一個(gè)優(yōu)先級(jí)屬性,返回最大值后再重新設(shè)置resource_request的優(yōu)先級(jí)屬性。
在構(gòu)建resource request對(duì)象時(shí)所有資源都是最低的,這個(gè)可以從構(gòu)造函數(shù)里面知道:
- ResourceRequest::ResourceRequest(const KURL& url)
- : url_(url),
- service_worker_mode_(WebURLRequest::ServiceWorkerMode::kAll),
- priority_(kResourceLoadPriorityLowest)
- /* 其它參數(shù)略 */ {}
但是同步請(qǐng)求在初始化的時(shí)候會(huì)先設(shè)置成最高:
- void FetchParameters::MakeSynchronous() {
- // Synchronous requests should always be max priority, lest they hang the
- // renderer.
- resource_request_.SetPriority(kResourceLoadPriorityHighest);
- resource_request_.SetTimeoutInterval(10);
- options_.synchronous_policy = kRequestSynchronously;
- }
以上就是基本的資源加載優(yōu)先級(jí)策略。
(2)轉(zhuǎn)換成Net的優(yōu)先級(jí)
這個(gè)是在渲染線程里面進(jìn)行的,上面提到的資源優(yōu)先級(jí)在發(fā)請(qǐng)求之前會(huì)被轉(zhuǎn)化成Net的優(yōu)先級(jí):
- resource_request->priority =
- ConvertWebKitPriorityToNetPriority(request.GetPriority());
資源優(yōu)先級(jí)對(duì)應(yīng)Net的優(yōu)先級(jí)關(guān)系如下所示:
畫成一個(gè)表:
Net Priority是請(qǐng)求資源的時(shí)候使用的,這個(gè)是在Chrome的IO線程里面進(jìn)行的, 我在《JS與多線程》的Chrome的多線程模型里面提到,每個(gè)頁面都有Renderer線程負(fù)責(zé)渲染頁面,而瀏覽器有IO線程,用來負(fù)責(zé)請(qǐng)求資源等。為什么IO線程不是放在每個(gè)頁面里面而是放在瀏覽器框架呢?因?yàn)檫@樣的好處是如果兩個(gè)頁面請(qǐng)求了相同資源的話,如果有緩存的話就能避免重復(fù)請(qǐng)求了。
上面的都是在渲染線程里面debug操作得到的數(shù)據(jù),為了能夠觀察資源請(qǐng)求的過程,需要切換到IO線程,而這兩個(gè)線程間的通信是通過Chrome封裝的Mojo框架進(jìn)行的。在Renderer線程會(huì)發(fā)一個(gè)消息給IO線程通知它:
- mojo::Message message(
- internal::kURLLoaderFactory_CreateLoaderAndStart_Name, kFlags, 0, 0, nullptr);
- // 對(duì)這個(gè)message進(jìn)行各種設(shè)置后(代碼略),調(diào)接收者的Accept函數(shù)
- ignore_result(receiver_->Accept(&message));
XCode里面可以看到這是在渲染線程RendererMain里操作的:
現(xiàn)在要切到Chrome的IO線程,把debug的方式改一下,如下選擇Chromium程序:
之前是使用Attach to Process把渲染進(jìn)程的PID傳進(jìn)來,因?yàn)槊總€(gè)頁面都是獨(dú)立的一個(gè)進(jìn)程,現(xiàn)在要改成debug Chromium進(jìn)程。然后在content/browser/loader/resource_scheduler.cc這個(gè)文件里的ShouldStartRequest函數(shù)里面打個(gè)斷點(diǎn),接著在Chromium里面打開一個(gè)網(wǎng)頁,就可以看到斷點(diǎn)生效了。在XCode里面可以看到當(dāng)前線程名稱叫Chrome_IOThread:
這與上面的描述一致。IO線程是如何利用優(yōu)先級(jí)決定要不要開始加載資源的呢?
(3)資源加載
上面提到的ShouldStartRequest這個(gè)函數(shù)是判斷當(dāng)前資源是否能開始加載了,如果能的話就準(zhǔn)備加載了,如果不能的話就繼續(xù)把它放到pending request隊(duì)列里面,如下代碼所示:
- void ScheduleRequest(const net::URLRequest& url_request,
- ScheduledResourceRequest* request) {
- SetRequestAttributes(request, DetermineRequestAttributes(request));
- ShouldStartReqResult should_start = ShouldStartRequest(request);
- if (should_start == START_REQUEST) {
- // New requests can be started synchronously without issue.
- StartRequest(request, START_SYNC, RequestStartTrigger::NONE);
- } else {
- pending_requests_.Insert(request);
- }
- }
一旦收到Mojo的加載資源消息就會(huì)調(diào)上面的ScheduleRequest函數(shù),除了收到消息之外,還有一個(gè)地方也會(huì)調(diào)用:
- void LoadAnyStartablePendingRequests(RequestStartTrigger trigger) {
- // We iterate through all the pending requests, starting with the highest
- // priority one.
- RequestQueue::NetQueue::iterator request_iter =
- pending_requests_.GetNextHighestIterator();
- while (request_iter != pending_requests_.End()) {
- ScheduledResourceRequest* request = *request_iter;
- ShouldStartReqResult query_result = ShouldStartRequest(request);
- if (query_result == START_REQUEST) {
- pending_requests_.Erase(request);
- StartRequest(request, START_ASYNC, trigger);
- }
- }
這個(gè)函數(shù)的特點(diǎn)是遍歷pending requests,每次取出優(yōu)先級(jí)最高的一個(gè)request,然后調(diào)ShouldStartRequest判斷是否能運(yùn)行了,如果能的話就把它從pending requests里面刪掉,然后運(yùn)行。
而這個(gè)函數(shù)會(huì)有三個(gè)地方會(huì)調(diào)用,一個(gè)是IO線程的循環(huán)判斷,只要還有未完成的任務(wù),就會(huì)觸發(fā)加載,第二個(gè)是當(dāng)有請(qǐng)求完成時(shí)會(huì)調(diào),第三個(gè)是要插入body標(biāo)簽的時(shí)候。所以主要總共有三個(gè)地方會(huì)觸發(fā)加載:
(1)收到來自渲染線程IPC::Mojo的請(qǐng)求加載資源的消息
(2)每個(gè)請(qǐng)求完成之后,觸發(fā)加載pending requests里還未加載的請(qǐng)求
(3)IO線程定時(shí)循環(huán)未完成的任務(wù),觸發(fā)加載
- <!DOCType html>
- <html>
- <head>
- <meta charset="utf-8">
- <link rel="icon" href="4.png">
- <img src="0.png">
- <img src="1.png">
- <link rel="stylesheet" href="1.css">
- <link rel="stylesheet" href="2.css">
- <link rel="stylesheet" href="3.css">
- <link rel="stylesheet" href="4.css">
- <link rel="stylesheet" href="5.css">
- <link rel="stylesheet" href="6.css">
- <link rel="stylesheet" href="7.css">
- </head>
- <body>
- <p>hello</p>
- <img src="2.png">
- <img src="3.png">
- <img src="4.png">
- <img src="5.png">
- <img src="6.png">
- <img src="7.png">
- <img src="8.png">
- <img src="9.png">
- <script src="1.js"></script>
- <script src="2.js"></script>
- <script src="3.js"></script>
- <img src="3.png">
- <script>
- !function(){
- let xhr = new XMLHttpRequest();
- xhr.open("GET", "https://baidu.com");
- xhr.send();
- document.write("hi");
- }();
- </script>
- <link rel="stylesheet" href="9.css">
- </body>
- </html>
知道了觸發(fā)加載機(jī)制之的,接著研究具體優(yōu)先加載的過程,用以下html做為demo:
然后把Chrome的網(wǎng)絡(luò)速度調(diào)為Fast 3G,讓加載速度降低,以便更好地觀察這個(gè)過程,結(jié)果如下圖所示:
從上圖可以發(fā)現(xiàn)以下特點(diǎn):
(1)每個(gè)域每次最多同時(shí)加載6個(gè)資源(http/1.1)
(2)CSS具有最高的優(yōu)先級(jí),最先加載,即使是放在最后面9.css也是比前面資源先開始加載
(3)JS比圖片優(yōu)先加載,即使出現(xiàn)得比圖片晚
(4)只有等CSS都加載完了,才能加載其它的資源,即使這個(gè)時(shí)候沒有達(dá)到6個(gè)的限制
(5)head里面的非高優(yōu)化級(jí)的資源最多能先加載一張(0.png)
(6)xhr的資源雖然具有高優(yōu)先級(jí),但是由于它是排在3.js后面的,JS的執(zhí)行是同步的,所以它排得比較靠后,如果把它排在1.js前面,那么它也會(huì)比圖片先加載。
為什么是這樣呢?我們從源碼尋找答案。
首先認(rèn)清幾個(gè)概念,請(qǐng)求可分為delayable和none-delayable兩種:
- // The priority level below which resources are considered to be delayable.
- static const net::RequestPriority
- kDelayablePriorityThreshold = net::MEDIUM;
在優(yōu)先級(jí)在Medium以下的為delayable,即可推遲的,而大于等于Medium的為不可delayable的。從剛剛我們總結(jié)的表可以看出:css/js是不可推遲的,而圖片、preload的js為可推遲加載:
還有一種是layout-blocking的請(qǐng)求:
- // The priority level above which resources are considered layout-blocking if
- // the html_body has not started.
- static const net::RequestPriority
- kLayoutBlockingPriorityThreshold = net::MEDIUM;
這是當(dāng)還沒有渲染body標(biāo)簽,并且優(yōu)先級(jí)在Medium之上的如CSS的請(qǐng)求。
然后,上面提到的ShouldStartRequest函數(shù),這個(gè)函數(shù)是規(guī)劃資源加載順序最主要的函數(shù),從源碼注釋可以知道它大概的過程:
- // ShouldStartRequest is the main scheduling algorithm.
- //
- // Requests are evaluated on five attributes:
- //
- // 1. Non-delayable requests:
- // * Synchronous requests.
- // * Non-HTTP[S] requests.
- //
- // 2. Requests to request-priority-capable origin servers.
- //
- // 3. High-priority requests:
- // * Higher priority requests (> net::LOW).
- //
- // 4. Layout-blocking requests:
- // * High-priority requests (> net::MEDIUM) initiated before the renderer has
- // a <body>.
- //
- // 5. Low priority requests
- //
- // The following rules are followed:
- //
- // All types of requests:
- // * Non-delayable, High-priority and request-priority capable requests are
- // issued immediately.
- // * Low priority requests are delayable.
- // * While kInFlightNonDelayableRequestCountPerClientThreshold(=1)
- // layout-blocking requests are loading or the body tag has not yet been
- // parsed, limit the number of delayable requests that may be in flight
- // to kMaxNumDelayableWhileLayoutBlockingPerClient(=1).
- // * If no high priority or layout-blocking requests are in flight, start
- // loading delayable requests.
- // * Never exceed 10 delayable requests in flight per client.
- // * Never exceed 6 delayable requests for a given host.
從上面的注釋可以得到以下信息:
(1)高優(yōu)先級(jí)的資源(>=Medium)、同步請(qǐng)求和非http(s)的請(qǐng)求能夠立刻加載
(2)只要有一個(gè)layout blocking的資源在加載,最多只能加載一個(gè)delayable的資源,這個(gè)就解釋了為什么0.png能夠先加載
(3)只有當(dāng)layout blocking和high priority的資源加載完了,才能開始加載delayable的資源,這個(gè)就解釋了為什么要等CSS加載完了才能加載其它的js/圖片。
(4)同時(shí)加載的delayable資源同一個(gè)域只能有6個(gè),同一個(gè)client即同一個(gè)頁面最多只能有10個(gè),否則要進(jìn)行排隊(duì)。
注意這里說的開始加載,并不是說能夠開始請(qǐng)求建立連接了。源碼里面叫in flight,在飛行中,而不是叫in request之類的,能夠進(jìn)行in flight的請(qǐng)求是指那些不用queue的請(qǐng)求,如下圖:
白色條是指queue的時(shí)間段,而灰色的是已經(jīng)in flight了但受到同域只能最多只能建立6個(gè)TCP連接等的影響而進(jìn)入的stalled狀態(tài),綠色是TTFB(Time to First Byte)從開始建立TCP連接到收到第一個(gè)字節(jié)的時(shí)間,藍(lán)色是下載的時(shí)間。
我們已經(jīng)解釋了大部分加載的特點(diǎn)的原因,對(duì)著上面那張圖可以再重述一次:
(1)由于1.css到9.css這幾個(gè)CSS文件是high priority或者是none delayable的,所以馬上in flight,但是還受到了同一個(gè)域最多只能有6個(gè)的限制,所以6/7/9.css這三個(gè)進(jìn)入stalled的狀態(tài)
(2)1.css到5.css是layout-blocking的,所以最多只能再加載一個(gè)delayable的0.png,在它相鄰的1.png就得排隊(duì)了
(3)等到high priority和layout-blocking的資源7.css/9.css/1.js加載完了,就開始加載delayable的資源,主要是preload的js和圖片
這里有個(gè)問題,為什么1.js是high priority的而2.js和3.js卻是delayable的?為此在源碼的ShouldStartRequest函數(shù)里面添加一些代碼,把每次判斷請(qǐng)求的一些關(guān)鍵信息打印出來:
- LOG(INFO) << "url: " << url_request.url().spec() << " priority: " << url_request.priority()
- << " has_html_body_: " << has_html_body_ << " delayable: "
- << RequestAttributesAreSet(request->attributes(), kAttributeDelayable);
把打印出來的信息按順序畫成以下表格:
1.js的優(yōu)先級(jí)一開始是Low的,即是delayable的,但是后面又變成了Medium就不是delayable了,是high priority,為什么它的優(yōu)先級(jí)能夠提高呢?一開始是Low是因?yàn)樗峭茰y(cè)加載的,所以是優(yōu)先級(jí)比較低,但是當(dāng)DOM構(gòu)建到那里的時(shí)候它就不是preload的,變成正常的JS加載了,所以它的優(yōu)先級(jí)變成了Medium,這個(gè)可以從has_html_body標(biāo)簽進(jìn)行推測(cè),而2.js要等到1.js下載和解析完,它能算是正常加載,否則還是推測(cè)加載,因此它的優(yōu)先級(jí)沒有得到提高。
本次解讀到這里告一段落,我們得到了有3種原因會(huì)阻止加載資源,包括CSP、Mixed Content、Origin block,CSP是自己手動(dòng)設(shè)置的一些限制,Mixed Content是https頁面不允許加載http的內(nèi)容,Origin Block主要是svg的href只能是同源的資源。還知道了瀏覽器把資源歸成CSS/Font/JS/Image等幾類,總共有5種優(yōu)先級(jí),從Lowest到Highest,每種資源都會(huì)設(shè)定一個(gè)優(yōu)先級(jí),總的來說CSS/Font/Frame和同步請(qǐng)求這四種的優(yōu)先級(jí)是最高的,不能推遲加載的,而正常加載的JS屬于高優(yōu)先級(jí),推測(cè)加載preload則優(yōu)先級(jí)會(huì)比較低,會(huì)推遲加載。并且如果有l(wèi)ayout blocking的請(qǐng)求的話,那么delayable的資源要等到高優(yōu)先級(jí)的加載完了才能進(jìn)行加載。已經(jīng)開始加載的資源還可能會(huì)處于stalled的狀態(tài),因?yàn)槊總€(gè)域同時(shí)建立的TCP連接數(shù)是有限的。
但是我們還有很多問題沒有得到解決,例如:
(1)同源策略具體是怎樣處理的?
(2)優(yōu)先級(jí)是如何動(dòng)態(tài)改變的?
(3)http cache/service worker是如何影響資源加載的?
我們將嘗試在下一次解讀進(jìn)行回答,看源碼是一件比較費(fèi)時(shí)費(fèi)力的事情,本篇是研究了三個(gè)周末四五天的時(shí)間才得到的,而且為了避免錯(cuò)誤不會(huì)隨便進(jìn)行臆測(cè),基本上每個(gè)小點(diǎn)都是實(shí)際debug執(zhí)行和打印console得到的,經(jīng)過驗(yàn)證了才寫出來。但是由于看的深度有限和理解偏差,可能會(huì)有一些不全面的地方甚至錯(cuò)誤,但是從注釋可以看到有些地方為什么有這個(gè)判斷條件即使是源碼的維護(hù)者也不太確定。本篇解讀盡可能地實(shí)事求事。