從Chrome源碼看瀏覽器如何加載資源
對瀏覽器加載資源有很多不確定性,例如:
- css/font的資源的優(yōu)化級會比img高,資源的優(yōu)化級是怎么確定的呢?
- 資源優(yōu)先級又是如何影響加載的先后順序的?
- 有幾種情況可能會導(dǎo)致資源被阻止加載?
通過源碼可以找到答案。此次源碼解讀基于Chromium 64(10月28日更新的源碼)。
下面通過加載資源的步驟,依次說明。
1. 開始加載
通過以下命令打開Chromium,同時打開一個網(wǎng)頁:
- chromium --renderer-startup-dialog https://www.baidu.com
Chrome會在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的資源,這個是什么東西呢?這個是video的字幕,使用webvtt格式:
- <video controls poster="/images/sample.gif">
- <source src="sample.mp4" type="video/mp4">
- <track kind="captions" src="sampleCaptions.vtt" srclang="en">
- </video>
還有動態(tài)請求ajax屬于Raw類型。因?yàn)閍jax可以請求多種資源。
MainResource包括location即導(dǎo)航輸入地址得到的頁面、使用frame/iframe嵌套的、通過超鏈接點(diǎn)擊的頁面以及表單提交這幾種。
接著交給稍底層的ResourceFecher去加載,所有資源都是通過它加載:
- fetcher->RequestResource(
- params, RawResourceFactory(Resource::kMainResource), substitute_data)
在這個里面會先對請求做預(yù)處理。
2. 預(yù)處理請求
每發(fā)個請求會生成一個ResourceRequest對象,這個對象包含了http請求的所有信息:
包括url、http header、http body等,還有請求的優(yōu)先級信息等:
然后會根據(jù)頁面的加載策略對這個請求做一些預(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會做兩件事情,一件是檢查請求是否合法,第二件是把請求做些修改。如果檢查合法性返回kAbort或者kBlock,說明資源被廢棄了或者被阻止了,就不去加載了。
被block的原因可能有以下幾種:
- enum class ResourceRequestBlockedReason {
- kCSP, // CSP內(nèi)容安全策略檢查
- kMixedContent, // mixed content
- kOrigin, // secure origin
- kInspector, // devtools的檢查器
- kSubresourceFilter,
- kOther,
- kNone
- };
源碼里面會在這個函數(shù)做合法性檢查:
- blocked_reason = Context().CanRequest(/*參數(shù)省略*/);
- if (blocked_reason != ResourceRequestBlockedReason::kNone) {
- return kBlock;
- }
CanRequest函數(shù)會相應(yīng)地檢查以下內(nèi)容:
(1)CSP(Content Security Policy)內(nèi)容安全策略檢查
CSP是減少XSS攻擊一個策略。如果我們只允許加載自己域的圖片的話,可以加上下面這個meta標(biāo)簽:
- <meta http-equiv="Content-Security-Policy" content="img-src 'self';">
或者是后端設(shè)置這個http響應(yīng)頭。
self表示本域,如果加載其它域的圖片瀏覽器將會報錯:
所以這個可以防止一些XSS注入的跨域請求。
源碼里面會檢查該請求是否符合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沒有通過的話就會返回堵塞的原因。具體的檢查過程是根據(jù)不同的資源類型去獲取該類資源資源的CSP設(shè)定進(jìn)行比較。
接著會根據(jù)CSP的要求改變請求:
- ModifyRequestForCSP(request);
主要是升級http為https。
(2)upgrade-insecure-requests
如果設(shè)定了以下CSP規(guī)則:
- <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
那么會將網(wǎng)頁的http請求強(qiáng)制升級為https,這是通過改變request對象實(shí)現(xiàn)的:
- url.SetProtocol("https");
- if (url.Port() == 80)
- url.SetPort(443);
- resource_request.SetURL(url);
包括改變url的協(xié)議和端口號。
(3)Mixed Content混合內(nèi)容block
在https的網(wǎng)站請求http的內(nèi)容就是Mixed Content,例如加載一個http的JS腳本,這種請求通常會被瀏覽器堵塞掉,因?yàn)閔ttp是沒有加密的,容易受到中間人的攻擊,如修改JS的內(nèi)容,從而控制整個https的頁面,而圖片之類的資源即使內(nèi)容被修改可能只是展示出問題,所以默認(rèn)沒有block掉。源碼里面會檢查Mixed Content的內(nèi)容:
- if (ShouldBlockFetchByMixedContentCheck(request_context, frame_type,
- resource_request.GetRedirectStatus(),
- url, reporting_policy))
- return ResourceRequestBlockedReason::kMixedContent;
在源碼里面,以下4種資源是optionally-blockable(被動混合內(nèi)容):
- // "Optionally-blockable" mixed content
- case WebURLRequest::kRequestContextAudio:
- case WebURLRequest::kRequestContextFavicon:
- case WebURLRequest::kRequestContextImage:
- case WebURLRequest::kRequestContextVideo:
- return WebMixedContentContextType::kOptionallyBlockable;
什么叫被動混合內(nèi)容呢?W3C文檔是這么說的:那些不會打破頁面重要部分,風(fēng)險比較低的,但是使用頻率又比較高的Mixed Content內(nèi)容。
而剩下的其它所有資源幾乎都是blockable的,包括JS/CSS/Iframe/XMLHttpRequest等:
我們注意到img srcset里的資源也是默認(rèn)會被阻止的,即下面的img會被block:
- <img srcset="http://fedren.com/test-1x.png 1x, http://fedren.com/test-2x.png 2x" alt>
但是使用src的不會被block:
- <img src="http://fedren.com/images/sell/icon-home.png" alt>
如下圖所示:
這就是optionally-blockable和blocakable資源的區(qū)分。
對于被動混合內(nèi)容,如果設(shè)置strick mode:
- <meta http-equiv="Content-Security-Policy" content="block-all-mixed-content">
那么即使是optionally的也會被block掉:
- case WebMixedContentContextType::kOptionallyBlockable:
- allowed = !strict_mode;
- if (allowed) {
- content_settings_client->PassiveInsecureContentFound(url);
- client->DidDisplayInsecureContent();
- }
- break;
上面代碼,如果strick_mode是true,allowed就是false,被動混合內(nèi)容就會被阻止。
而對于主動混合內(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行會去判斷當(dāng)前的client即當(dāng)前頁面的設(shè)置是否允許加載blockable的資源。另外源碼注釋還提到了一種特殊的情況,就是a.com的頁面包含了b.com的頁面,b.com允許加載blockable的資源,a.com在非strick mode的時候頁面是允許加載的,但是如果a.com是strick mode,那么將不允許加載。
并且如果頁面設(shè)置了strick mode,用戶設(shè)置的允許blockable資源加載的設(shè)置將會失效:
- // 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();
這個主要是svg使用use的獲取svg資源的時候必須不能跨域,如下以下資源將會被阻塞:
- <svg>
- <use href="http://cdn.test.com/images/logo.svg#abc"></use>
- </svg>
如下圖所示:
并且控制臺會打?。?/p>
源碼里面會對這種use link加載的svg做一個檢驗(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é)議、域名、端口號都一樣則通過檢查。需要這里和同源策略是兩碼事,這里的源阻塞是連請求都發(fā)不出去,而同源策略只是阻塞請求的返回結(jié)果。
svg的use外鏈一般是用來做svg的雪碧圖的,但是為什么需要同源呢,如果不同源會有什么不安全的因素?這里我也不清楚,暫時沒查到,W3C只是說明了需要同源,但沒有給出原因。
以上就是3種主要的block的原因。在預(yù)處理請求里面除了判斷資源有沒有被block或者abort(abort的原因通常是url不合法),還會計算資源的加載優(yōu)先級。
3. 資源優(yōu)先級
(1)計算資源加載優(yōu)先級
通過調(diào)用以下函數(shù)設(shè)定:
- resource_request.SetPriority(ComputeLoadPriority(
- resource_type, params.GetResourceRequest(), ResourcePriority::kNotVisible,
- params.Defer(), params.GetSpeculativePreloadType(),
- params.IsLinkPreload()));
我們來看一下這個函數(shù)里面是怎么計算當(dāng)前資源的優(yōu)先級的。
首先每個資源都有一個默認(rèn)的優(yōu)先級,這個優(yōu)先級做為初始化值:
- ResourceLoadPriority priority = TypeToPriority(type);
不同類型的資源優(yōu)先級是這么定義的:
- 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)先級總共分為五級:very-high、high、medium、low、very-low,其中MainRescource頁面、CSS、字體這三個的優(yōu)先級是最高的,然后就是Script、Ajax這種,而圖片、音視頻的默認(rèn)優(yōu)先級是比較低的,最低的是prefetch預(yù)加載的資源。
什么是prefetch的資源呢?有時候你可能需要讓一些資源先加載好等著用,例如用戶輸入出錯的時候在輸入框右邊顯示一個X的圖片,如果等要顯示的時候再去加載就會有延時,這個時候可以用一個link標(biāo)簽:
- <link rel="prefetch" href="image.png">
瀏覽器空閑的時候就會去加載。另外還可以預(yù)解析DNS:
- <link rel="dns-prefetch" href="https://cdn.test.com">
預(yù)建立TCP連接:
- <link rel="preconnect" href="https://cdn.chime.me">
后面這兩個不屬于加載資源,這里順便提一下。
注意上面的switch-case設(shè)定資源優(yōu)先級有一個順序,如果既是script又是prefetch的話得到的優(yōu)化級是high,而不是prefetch的very low,因?yàn)閜refetch是最后一個判斷。所以在設(shè)定了資源默認(rèn)的優(yōu)先級之后,會再對一些情況做一些調(diào)整,主要是對prefetch/preload的資源。包括:
a)降低preload的字體的優(yōu)先級
如下代碼:
- // A preloaded font should not take precedence over critical CSS or
- // parser-blocking scripts.
- if (type == Resource::kFont && is_link_preload)
- priority = kResourceLoadPriorityHigh;
會把預(yù)加載字體的優(yōu)先級從very-high變成high
b)降低defer/async的script的優(yōu)先級
如下代碼:
- 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)先級會變成最低。
4)頁面底部preload的script優(yōu)先級變成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)先級調(diào)成最低(上面第3小點(diǎn)),否則如果是preload的script,并且如果頁面已經(jīng)加載了一張圖片就認(rèn)為這個script是在頁面偏底部的位置,就把它的優(yōu)先級調(diào)成medium。通過一個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)先級會偏低。
什么叫preload呢?preload不同于prefetch的,在早期瀏覽器,script資源是阻塞加載的,當(dāng)頁面遇到一個script,那么要等這個script下載和執(zhí)行完了,才會繼續(xù)解析剩下的DOM結(jié)構(gòu),也就是說script是串行加載的,并且會堵塞頁面其它資源的加載,這樣會導(dǎo)致頁面整體的加載速度很慢,所以早在2008年的時候?yàn)g覽器出了一個推測加載(speculative preload)策略,即遇到script的時候,DOM會停止構(gòu)建,但是會繼續(xù)去搜索頁面需要加載的資源,如看下后續(xù)的html有沒有img/script標(biāo)簽,先進(jìn)行預(yù)加載,而不用等到構(gòu)建DOM的時候才去加載。這樣大大提高了頁面整體的加載速度。
5)把同步即堵塞加載的資源的優(yōu)先級調(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對象里面的優(yōu)先最級是最高的,所以本來是hight的ajax同步請求在最后return的時候會變成very-high。
這里是取了兩個值的最大值,第一個值是上面進(jìn)行各種判斷得到的priority,第二個在初始這個ResourceRequest對象本身就有的一個優(yōu)先級屬性,返回最大值后再重新設(shè)置resource_request的優(yōu)先級屬性。
在構(gòu)建resource request對象時所有資源都是最低的,這個可以從構(gòu)造函數(shù)里面知道:
- ResourceRequest::ResourceRequest(const KURL& url)
- : url_(url),
- service_worker_mode_(WebURLRequest::ServiceWorkerMode::kAll),
- priority_(kResourceLoadPriorityLowest)
- /* 其它參數(shù)略 */ {}
但是同步請求在初始化的時候會先設(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)先級策略。
(2)轉(zhuǎn)換成Net的優(yōu)先級
這個是在渲染線程里面進(jìn)行的,上面提到的資源優(yōu)先級在發(fā)請求之前會被轉(zhuǎn)化成Net的優(yōu)先級:
- resource_request->priority =
- ConvertWebKitPriorityToNetPriority(request.GetPriority());
資源優(yōu)先級對應(yīng)Net的優(yōu)先級關(guān)系如下所示:
畫成一個表:
Net Priority是請求資源的時候使用的,這個是在Chrome的IO線程里面進(jìn)行的, 我在《JS與多線程》的Chrome的多線程模型里面提到,每個頁面都有Renderer線程負(fù)責(zé)渲染頁面,而瀏覽器有IO線程,用來負(fù)責(zé)請求資源等。為什么IO線程不是放在每個頁面里面而是放在瀏覽器框架呢?因?yàn)檫@樣的好處是如果兩個頁面請求了相同資源的話,如果有緩存的話就能避免重復(fù)請求了。
上面的都是在渲染線程里面debug操作得到的數(shù)據(jù),為了能夠觀察資源請求的過程,需要切換到IO線程,而這兩個線程間的通信是通過Chrome封裝的Mojo框架進(jìn)行的。在Renderer線程會發(fā)一個消息給IO線程通知它:
- mojo::Message message(
- internal::kURLLoaderFactory_CreateLoaderAndStart_Name, kFlags, 0, 0, nullptr);
- // 對這個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)槊總€頁面都是獨(dú)立的一個進(jìn)程,現(xiàn)在要改成debug Chromium進(jìn)程。然后在content/browser/loader/resource_scheduler.cc這個文件里的ShouldStartRequest函數(shù)里面打個斷點(diǎn),接著在Chromium里面打開一個網(wǎng)頁,就可以看到斷點(diǎn)生效了。在XCode里面可以看到當(dāng)前線程名稱叫Chrome_IOThread:
這與上面的描述一致。IO線程是如何利用優(yōu)先級決定要不要開始加載資源的呢?
(3)資源加載
上面提到的ShouldStartRequest這個函數(shù)是判斷當(dāng)前資源是否能開始加載了,如果能的話就準(zhǔn)備加載了,如果不能的話就繼續(xù)把它放到pending request隊列里面,如下代碼所示:
- 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的加載資源消息就會調(diào)上面的ScheduleRequest函數(shù),除了收到消息之外,還有一個地方也會調(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);
- }
- }
這個函數(shù)的特點(diǎn)是遍歷pending requests,每次取出優(yōu)先級最高的一個request,然后調(diào)ShouldStartRequest判斷是否能運(yùn)行了,如果能的話就把它從pending requests里面刪掉,然后運(yùn)行。
而這個函數(shù)會有三個地方會調(diào)用,一個是IO線程的循環(huán)判斷,只要還有未完成的任務(wù),就會觸發(fā)加載,第二個是當(dāng)有請求完成時會調(diào),第三個是要插入body標(biāo)簽的時候。所以主要總共有三個地方會觸發(fā)加載:
(1)收到來自渲染線程IPC::Mojo的請求加載資源的消息
(2)每個請求完成之后,觸發(fā)加載pending requests里還未加載的請求
(3)IO線程定時循環(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,讓加載速度降低,以便更好地觀察這個過程,結(jié)果如下圖所示:
從上圖可以發(fā)現(xiàn)以下特點(diǎn):
(1)每個域每次最多同時加載6個資源(http/1.1)
(2)CSS具有最高的優(yōu)先級,最先加載,即使是放在最后面9.css也是比前面資源先開始加載
(3)JS比圖片優(yōu)先加載,即使出現(xiàn)得比圖片晚
(4)只有等CSS都加載完了,才能加載其它的資源,即使這個時候沒有達(dá)到6個的限制
(5)head里面的非高優(yōu)化級的資源最多能先加載一張(0.png)
(6)xhr的資源雖然具有高優(yōu)先級,但是由于它是排在3.js后面的,JS的執(zhí)行是同步的,所以它排得比較靠后,如果把它排在1.js前面,那么它也會比圖片先加載。
為什么是這樣呢?我們從源碼尋找答案。
首先認(rèn)清幾個概念,請求可分為delayable和none-delayable兩種:
- // The priority level below which resources are considered to be delayable.
- static const net::RequestPriority
- kDelayablePriorityThreshold = net::MEDIUM;
在優(yōu)先級在Medium以下的為delayable,即可推遲的,而大于等于Medium的為不可delayable的。從剛剛我們總結(jié)的表可以看出:css/js是不可推遲的,而圖片、preload的js為可推遲加載:
還有一種是layout-blocking的請求:
- // 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)先級在Medium之上的如CSS的請求。
然后,上面提到的ShouldStartRequest函數(shù),這個函數(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)先級的資源(>=Medium)、同步請求和非http(s)的請求能夠立刻加載
(2)只要有一個layout blocking的資源在加載,最多只能加載一個delayable的資源,這個就解釋了為什么0.png能夠先加載
(3)只有當(dāng)layout blocking和high priority的資源加載完了,才能開始加載delayable的資源,這個就解釋了為什么要等CSS加載完了才能加載其它的js/圖片。
(4)同時加載的delayable資源同一個域只能有6個,同一個client即同一個頁面最多只能有10個,否則要進(jìn)行排隊。
注意這里說的開始加載,并不是說能夠開始請求建立連接了。源碼里面叫in flight,在飛行中,而不是叫in request之類的,能夠進(jìn)行in flight的請求是指那些不用queue的請求,如下圖:
白色條是指queue的時間段,而灰色的是已經(jīng)in flight了但受到同域只能最多只能建立6個TCP連接等的影響而進(jìn)入的stalled狀態(tài),綠色是TTFB(Time to First Byte)從開始建立TCP連接到收到第一個字節(jié)的時間,藍(lán)色是下載的時間。
我們已經(jīng)解釋了大部分加載的特點(diǎn)的原因,對著上面那張圖可以再重述一次:
(1)由于1.css到9.css這幾個CSS文件是high priority或者是none delayable的,所以馬上in flight,但是還受到了同一個域最多只能有6個的限制,所以6/7/9.css這三個進(jìn)入stalled的狀態(tài)
(2)1.css到5.css是layout-blocking的,所以最多只能再加載一個delayable的0.png,在它相鄰的1.png就得排隊了
(3)等到high priority和layout-blocking的資源7.css/9.css/1.js加載完了,就開始加載delayable的資源,主要是preload的js和圖片
這里有個問題,為什么1.js是high priority的而2.js和3.js卻是delayable的?為此在源碼的ShouldStartRequest函數(shù)里面添加一些代碼,把每次判斷請求的一些關(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)先級一開始是Low的,即是delayable的,但是后面又變成了Medium就不是delayable了,是high priority,為什么它的優(yōu)先級能夠提高呢?一開始是Low是因?yàn)樗峭茰y加載的,所以是優(yōu)先級比較低,但是當(dāng)DOM構(gòu)建到那里的時候它就不是preload的,變成正常的JS加載了,所以它的優(yōu)先級變成了Medium,這個可以從has_html_body標(biāo)簽進(jìn)行推測,而2.js要等到1.js下載和解析完,它能算是正常加載,否則還是推測加載,因此它的優(yōu)先級沒有得到提高。
本次解讀到這里告一段落,我們得到了有3種原因會阻止加載資源,包括CSP、Mixed Content、Origin block,CSP是自己手動設(shè)置的一些限制,Mixed Content是https頁面不允許加載http的內(nèi)容,Origin Block主要是svg的href只能是同源的資源。還知道了瀏覽器把資源歸成CSS/Font/JS/Image等幾類,總共有5種優(yōu)先級,從Lowest到Highest,每種資源都會設(shè)定一個優(yōu)先級,總的來說CSS/Font/Frame和同步請求這四種的優(yōu)先級是最高的,不能推遲加載的,而正常加載的JS屬于高優(yōu)先級,推測加載preload則優(yōu)先級會比較低,會推遲加載。并且如果有l(wèi)ayout blocking的請求的話,那么delayable的資源要等到高優(yōu)先級的加載完了才能進(jìn)行加載。已經(jīng)開始加載的資源還可能會處于stalled的狀態(tài),因?yàn)槊總€域同時建立的TCP連接數(shù)是有限的。
但是我們還有很多問題沒有得到解決,例如:
(1)同源策略具體是怎樣處理的?
(2)優(yōu)先級是如何動態(tài)改變的?
(3)http cache/service worker是如何影響資源加載的?
我們將嘗試在下一次解讀進(jìn)行回答,看源碼是一件比較費(fèi)時費(fèi)力的事情,本篇是研究了三個周末四五天的時間才得到的,而且為了避免錯誤不會隨便進(jìn)行臆測,基本上每個小點(diǎn)都是實(shí)際debug執(zhí)行和打印console得到的,經(jīng)過驗(yàn)證了才寫出來。但是由于看的深度有限和理解偏差,可能會有一些不全面的地方甚至錯誤,但是從注釋可以看到有些地方為什么有這個判斷條件即使是源碼的維護(hù)者也不太確定。本篇解讀盡可能地實(shí)事求事。