從Chrome源碼看瀏覽器如何加載資源

對瀏覽器加載資源有不少不肯定性,例如:javascript

  1. css/font的資源的優化級會比img高,資源的優化級是怎麼肯定的呢?
  2. 資源優先級又是如何影響加載的前後順序的?
  3. 有幾種狀況可能會致使資源被阻止加載?

經過源碼能夠找到答案。這次源碼解讀基於Chromium 64(10月28日更新的源碼)。css

下面經過加載資源的步驟,依次說明。html

1. 開始加載

經過如下命令打開Chromium,同時打開一個網頁:java

chromium --renderer-startup-dialog https://www.baidu.com
複製代碼

Chrome會在DocumentLoader.cpp裏面經過如下代碼去加載:git

main_resource_ =
      RawResource::FetchMainResource(fetch_params, Fetcher(), substitute_data_);複製代碼

頁面資源屬於MainRescouce,Chrome把Rescource歸爲如下幾種:github

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以外,咱們發現還有像textTrack的資源,這個是什麼東西呢?這個是video的字幕,使用webvtt格式:web

<video controls poster="/images/sample.gif"> <source src="sample.mp4" type="video/mp4"> <track kind="captions" src="sampleCaptions.vtt" srclang="en"> </video>複製代碼

還有動態請求ajax屬於Raw類型。由於ajax能夠請求多種資源。ajax

MainResource包括location即導航輸入地址獲得的頁面、使用frame/iframe嵌套的、經過超連接點擊的頁面以及表單提交這幾種。後端

接着交給稍底層的ResourceFecher去加載,全部資源都是經過它加載:跨域

fetcher->RequestResource(
      params, RawResourceFactory(Resource::kMainResource), substitute_data)複製代碼

在這個裏面會先對請求作預處理。

2. 預處理請求

每發個請求會生成一個ResourceRequest對象,這個對象包含了http請求的全部信息:

包括url、http header、http body等,還有請求的優先級信息等:

而後會根據頁面的加載策略對這個請求作一些預處理,以下代碼:

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內容安全策略檢查
  kMixedContent,     // mixed content
  kOrigin,           // secure origin
  kInspector,        // devtools的檢查器
  kSubresourceFilter,
  kOther,
  kNone
};複製代碼

源碼裏面會在這個函數作合法性檢查:

blocked_reason = Context().CanRequest(/*參數省略*/);
  if (blocked_reason != ResourceRequestBlockedReason::kNone) {
    return kBlock;
  }複製代碼

CanRequest函數會相應地檢查如下內容:

(1)CSP(Content Security Policy)內容安全策略檢查

CSP是減小XSS攻擊一個策略。若是咱們只容許加載本身域的圖片的話,能夠加上下面這個meta標籤:

<meta http-equiv="Content-Security-Policy" content="img-src 'self';">複製代碼

或者是後端設置這個http響應頭。

self表示本域,若是加載其它域的圖片瀏覽器將會報錯:

因此這個能夠防止一些XSS注入的跨域請求。

源碼裏面會檢查該請求是否符合CSP的設定要求:

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沒有經過的話就會返回堵塞的緣由。具體的檢查過程是根據不一樣的資源類型去獲取該類資源資源的CSP設定進行比較。

接着會根據CSP的要求改變請求:

ModifyRequestForCSP(request);複製代碼

主要是升級http爲https。

(2)upgrade-insecure-requests

若是設定了如下CSP規則:

<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">複製代碼

那麼會將網頁的http請求強制升級爲https,這是經過改變request對象實現的:

url.SetProtocol("https");
      if (url.Port() == 80)
        url.SetPort(443);
      resource_request.SetURL(url);複製代碼

包括改變url的協議和端口號。

(3)Mixed Content混合內容block

在https的網站請求http的內容就是Mixed Content,例如加載一個http的JS腳本,這種請求一般會被瀏覽器堵塞掉,由於http是沒有加密的,容易受到中間人的攻擊,如修改JS的內容,從而控制整個https的頁面,而圖片之類的資源即便內容被修改可能只是展現出問題,因此默認沒有block掉。源碼裏面會檢查Mixed Content的內容:

if (ShouldBlockFetchByMixedContentCheck(request_context, frame_type,
                                          resource_request.GetRedirectStatus(),
                                          url, reporting_policy))
    return ResourceRequestBlockedReason::kMixedContent;複製代碼

在源碼裏面,如下4種資源是optionally-blockable(被動混合內容):

// "Optionally-blockable" mixed content
    case WebURLRequest::kRequestContextAudio:
    case WebURLRequest::kRequestContextFavicon:
    case WebURLRequest::kRequestContextImage:
    case WebURLRequest::kRequestContextVideo:
      return WebMixedContentContextType::kOptionallyBlockable;複製代碼

什麼叫被動混合內容呢?W3C文檔是這麼說的:那些不會打破頁面重要部分,風險比較低的,可是使用頻率又比較高的Mixed Content內容。

而剩下的其它全部資源幾乎都是blockable的,包括JS/CSS/Iframe/XMLHttpRequest等:

咱們注意到img srcset裏的資源也是默認會被阻止的,即下面的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資源的區分。

對於被動混合內容,若是設置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,被動混合內容就會被阻止。

而對於主動混合內容,若是用戶設置容許加載:

那麼也是能夠加載的:

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;複製代碼

代碼倒數第4行會去判斷當前的client即當前頁面的設置是否容許加載blockable的資源。另外源碼註釋還提到了一種特殊的狀況,就是a.com的頁面包含了b.com的頁面,b.com容許加載blockable的資源,a.com在非strick mode的時候頁面是容許加載的,可是若是a.com是strick mode,那麼將不容許加載。

而且若是頁面設置了strick mode,用戶設置的容許blockable資源加載的設置將會失效:

// 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();複製代碼

(4)Origin Block

這個主要是svg使用use的獲取svg資源的時候必須不能跨域,以下如下資源將會被阻塞:

<svg> <use href="http://cdn.test.com/images/logo.svg#abc"></use> </svg>複製代碼

以下圖所示:

而且控制檯會打印:

源碼裏面會對這種use link加載的svg作一個檢驗:

case Resource::kSVGDocument:
      if (!security_origin->CanRequest(url)) {
        PrintAccessDeniedMessage(url);
        return ResourceRequestBlockedReason::kOrigin;
      }
      break;複製代碼

具體檢驗CanRequest函數主要是檢查是否同源:

// We call isSameSchemeHostPort here instead of canAccess because we want
  // to ignore document.domain effects.
  if (IsSameSchemeHostPort(target_origin.get()))
    return true;

  return false;複製代碼

若是協議、域名、端口號都同樣則經過檢查。須要這裏和同源策略是兩碼事,這裏的源阻塞是連請求都發不出去,而同源策略只是阻塞請求的返回結果。

svg的use外鏈通常是用來作svg的雪碧圖的,可是爲何須要同源呢,若是不一樣源會有什麼不安全的因素?這裏我也不清楚,暫時沒查到,W3C只是說明了須要同源,但沒有給出緣由。

以上就是3種主要的block的緣由。在預處理請求裏面除了判斷資源有沒有被block或者abort(abort的緣由一般是url不合法),還會計算資源的加載優先級。

3. 資源優先級

(1)計算資源加載優先級

經過調用如下函數設定:

resource_request.SetPriority(ComputeLoadPriority(
      resource_type, params.GetResourceRequest(), ResourcePriority::kNotVisible,
      params.Defer(), params.GetSpeculativePreloadType(),
      params.IsLinkPreload()));複製代碼

咱們來看一下這個函數裏面是怎麼計算當前資源的優先級的。

首先每一個資源都有一個默認的優先級,這個優先級作爲初始化值:

ResourceLoadPriority priority = TypeToPriority(type);複製代碼

不一樣類型的資源優先級是這麼定義的:

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;
}複製代碼

能夠看到優先級總共分爲五級:very-high、high、medium、low、very-low,其中MainRescource頁面、CSS、字體這三個的優先級是最高的,而後就是Script、Ajax這種,而圖片、音視頻的默認優先級是比較低的,最低的是prefetch預加載的資源。

什麼是prefetch的資源呢?有時候你可能須要讓一些資源先加載好等着用,例如用戶輸入出錯的時候在輸入框右邊顯示一個X的圖片,若是等要顯示的時候再去加載就會有延時,這個時候能夠用一個link標籤:

<link rel="prefetch" href="image.png">複製代碼

瀏覽器空閒的時候就會去加載。另外還能夠預解析DNS:

<link rel="dns-prefetch" href="https://cdn.test.com">複製代碼

預創建TCP鏈接:

<link rel="preconnect" href="https://cdn.chime.me">複製代碼

後面這兩個不屬於加載資源,這裏順便提一下。

注意上面的switch-case設定資源優先級有一個順序,若是既是script又是prefetch的話獲得的優化級是high,而不是prefetch的very low,由於prefetch是最後一個判斷。因此在設定了資源默認的優先級以後,會再對一些狀況作一些調整,主要是對prefetch/preload的資源。包括:

a)下降preload的字體的優先級

以下代碼:

// A preloaded font should not take precedence over critical CSS or
  // parser-blocking scripts.
  if (type == Resource::kFont && is_link_preload)
    priority = kResourceLoadPriorityHigh;複製代碼

會把預加載字體的優先級從very-high變成high

b)下降defer/async的script的優先級

以下代碼:

if (type == Resource::kScript) {
    // Async/Defer: Low Priority (applies to both preload and parser-inserted)
    if (FetchParameters::kLazyLoad == defer_option) {
      priority = kResourceLoadPriorityLow;
    }
}複製代碼

script若是是defer的話,那麼它優先級會變成最低。

4)頁面底部preload的script優先級變成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那麼優先級調成最低(上面第3小點),不然若是是preload的script,而且若是頁面已經加載了一張圖片就認爲這個script是在頁面偏底部的位置,就把它的優先級調成medium。經過一個flag決定是否已經加載過第一張圖片了:

// 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的圖片前認爲是early,而在後面認爲是late,late的script的優先級會偏低。

什麼叫preload呢?preload不一樣於prefetch的,在早期瀏覽器,script資源是阻塞加載的,當頁面遇到一個script,那麼要等這個script下載和執行完了,纔會繼續解析剩下的DOM結構,也就是說script是串行加載的,而且會堵塞頁面其它資源的加載,這樣會致使頁面總體的加載速度很慢,因此早在2008年的時候瀏覽器出了一個推測加載(speculative preload)策略,即遇到script的時候,DOM會中止構建,可是會繼續去搜索頁面須要加載的資源,如看下後續的html有沒有img/script標籤,先進行預加載,而不用等到構建DOM的時候纔去加載。這樣大大提升了頁面總體的加載速度。

5)把同步即堵塞加載的資源的優先級調成最高

以下代碼:

// 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對象裏面的優先最級是最高的,因此原本是hight的ajax同步請求在最後return的時候會變成very-high。

這裏是取了兩個值的最大值,第一個值是上面進行各類判斷獲得的priority,第二個在初始這個ResourceRequest對象自己就有的一個優先級屬性,返回最大值後再從新設置resource_request的優先級屬性。

在構建resource request對象時全部資源都是最低的,這個能夠從構造函數裏面知道:

ResourceRequest::ResourceRequest(const KURL& url)
    : url_(url),
      service_worker_mode_(WebURLRequest::ServiceWorkerMode::kAll),
      priority_(kResourceLoadPriorityLowest)
      /* 其它參數略 */ {}複製代碼

可是同步請求在初始化的時候會先設置成最高:

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;
}複製代碼

以上就是基本的資源加載優先級策略。

(2)轉換成Net的優先級

這個是在渲染線程裏面進行的,上面提到的資源優先級在發請求以前會被轉化成Net的優先級:

resource_request->priority =
      ConvertWebKitPriorityToNetPriority(request.GetPriority());複製代碼

資源優先級對應Net的優先級關係以下所示:

畫成一個表:

Net Priority是請求資源的時候使用的,這個是在Chrome的IO線程裏面進行的, 我在《JS與多線程》的Chrome的多線程模型裏面提到,每一個頁面都有Renderer線程負責渲染頁面,而瀏覽器有IO線程,用來負責請求資源等。爲何IO線程不是放在每一個頁面裏面而是放在瀏覽器框架呢?由於這樣的好處是若是兩個頁面請求了相同資源的話,若是有緩存的話就能避免重複請求了。

上面的都是在渲染線程裏面debug操做獲得的數據,爲了可以觀察資源請求的過程,須要切換到IO線程,而這兩個線程間的通訊是經過Chrome封裝的Mojo框架進行的。在Renderer線程會發一個消息給IO線程通知它:

mojo::Message message( internal::kURLLoaderFactory_CreateLoaderAndStart_Name, kFlags, 0, 0, nullptr);
 // 對這個message進行各類設置後(代碼略),調接收者的Accept函數 
 ignore_result(receiver_->Accept(&message));複製代碼

XCode裏面能夠看到這是在渲染線程RendererMain裏操做的:

如今要切到Chrome的IO線程,把debug的方式改一下,以下選擇Chromium程序:

以前是使用Attach to Process把渲染進程的PID傳進來,由於每一個頁面都是獨立的一個進程,如今要改爲debug Chromium進程。而後在content/browser/loader/resource_scheduler.cc這個文件裏的ShouldStartRequest函數裏面打個斷點,接着在Chromium裏面打開一個網頁,就能夠看到斷點生效了。在XCode裏面能夠看到當前線程名稱叫Chrome_IOThread:

這與上面的描述一致。IO線程是如何利用優先級決定要不要開始加載資源的呢?

(3)資源加載

上面提到的ShouldStartRequest這個函數是判斷當前資源是否能開始加載了,若是能的話就準備加載了,若是不能的話就繼續把它放到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的加載資源消息就會調上面的ScheduleRequest函數,除了收到消息以外,還有一個地方也會調用:

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);
      }
  }複製代碼

這個函數的特色是遍歷pending requests,每次取出優先級最高的一個request,而後調ShouldStartRequest判斷是否能運行了,若是能的話就把它從pending requests裏面刪掉,而後運行。

而這個函數會有三個地方會調用,一個是IO線程的循環判斷,只要還有未完成的任務,就會觸發加載,第二個是當有請求完成時會調,第三個是要插入body標籤的時候。因此主要總共有三個地方會觸發加載:

(1)收到來自渲染線程IPC::Mojo的請求加載資源的消息

(2)每一個請求完成以後,觸發加載pending requests裏還未加載的請求

(3)IO線程定時循環未完成的任務,觸發加載


知道了觸發加載機制之的,接着研究具體優先加載的過程,用如下html作爲demo:

<!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>複製代碼

而後把Chrome的網絡速度調爲Fast 3G,讓加載速度下降,以便更好地觀察這個過程,結果以下圖所示:

從上圖能夠發現如下特色:

(1)每一個域每次最多同時加載6個資源(http/1.1)

(2)CSS具備最高的優先級,最早加載,即便是放在最後面9.css也是比前面資源先開始加載

(3)JS比圖片優先加載,即便出現得比圖片晚

(4)只有等CSS都加載完了,才能加載其它的資源,即便這個時候沒有達到6個的限制

(5)head裏面的非高優化級的資源最多能先加載一張(0.png)

(6)xhr的資源雖然具備高優先級,可是因爲它是排在3.js後面的,JS的執行是同步的,因此它排得比較靠後,若是把它排在1.js前面,那麼它也會比圖片先加載。

爲何是這樣呢?咱們從源碼尋找答案。


首先認清幾個概念,請求可分爲delayable和none-delayable兩種:

// The priority level below which resources are considered to be delayable.
static const net::RequestPriority
    kDelayablePriorityThreshold = net::MEDIUM;複製代碼

在優先級在Medium如下的爲delayable,便可推遲的,而大於等於Medium的爲不可delayable的。從剛剛咱們總結的表能夠看出: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;複製代碼

這是當尚未渲染body標籤,而且優先級在Medium之上的如CSS的請求。

而後,上面提到的ShouldStartRequest函數,這個函數是規劃資源加載順序最主要的函數,從源碼註釋能夠知道它大概的過程:

// 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)高優先級的資源(>=Medium)、同步請求和非http(s)的請求可以馬上加載

(2)只要有一個layout blocking的資源在加載,最多隻能加載一個delayable的資源,這個就解釋了爲何0.png可以先加載

(3)只有當layout blocking和high priority的資源加載完了,才能開始加載delayable的資源,這個就解釋了爲何要等CSS加載完了才能加載其它的js/圖片。

(4)同時加載的delayable資源同一個域只能有6個,同一個client即同一個頁面最多隻能有10個,不然要進行排隊。

注意這裏說的開始加載,並非說可以開始請求創建鏈接了。源碼裏面叫in flight,在飛行中,而不是叫in request之類的,可以進行in flight的請求是指那些不用queue的請求,以下圖:

白色條是指queue的時間段,而灰色的是已經in flight了但受到同域只能最多隻能創建6個TCP鏈接等的影響而進入的stalled狀態,綠色是TTFB(Time to First Byte)從開始創建TCP鏈接到收到第一個字節的時間,藍色是下載的時間。

咱們已經解釋了大部分加載的特色的緣由,對着上面那張圖能夠再重述一次:
(1)因爲1.css到9.css這幾個CSS文件是high priority或者是none delayable的,因此立刻in flight,可是還受到了同一個域最多隻能有6個的限制,因此6/7/9.css這三個進入stalled的狀態

(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函數裏面添加一些代碼,把每次判斷請求的一些關鍵信息打印出來:

LOG(INFO) << "url: " << url_request.url().spec() << " priority: " << url_request.priority()
    << " has_html_body_: " << has_html_body_ << " delayable: "
    << RequestAttributesAreSet(request->attributes(), kAttributeDelayable);複製代碼

把打印出來的信息按順序畫成如下表格:

1.js的優先級一開始是Low的,便是delayable的,可是後面又變成了Medium就不是delayable了,是high priority,爲何它的優先級可以提升呢?一開始是Low是由於它是推測加載的,因此是優先級比較低,可是當DOM構建到那裏的時候它就不是preload的,變成正常的JS加載了,因此它的優先級變成了Medium,這個能夠從has_html_body標籤進行推測,而2.js要等到1.js下載和解析完,它能算是正常加載,不然仍是推測加載,所以它的優先級沒有獲得提升。


本次解讀到這裏告一段落,咱們獲得了有3種緣由會阻止加載資源,包括CSP、Mixed Content、Origin block,CSP是本身手動設置的一些限制,Mixed Content是https頁面不容許加載http的內容,Origin Block主要是svg的href只能是同源的資源。還知道了瀏覽器把資源歸成CSS/Font/JS/Image等幾類,總共有5種優先級,從Lowest到Highest,每種資源都會設定一個優先級,總的來講CSS/Font/Frame和同步請求這四種的優先級是最高的,不能推遲加載的,而正常加載的JS屬於高優先級,推測加載preload則優先級會比較低,會推遲加載。而且若是有layout blocking的請求的話,那麼delayable的資源要等到高優先級的加載完了才能進行加載。已經開始加載的資源還可能會處於stalled的狀態,由於每一個域同時創建的TCP鏈接數是有限的。

可是咱們還有不少問題沒有獲得解決,例如:

(1)同源策略具體是怎樣處理的?

(2)優先級是如何動態改變的?

(3)http cache/service worker是如何影響資源加載的?

咱們將嘗試在下一次解讀進行回答,看源碼是一件比較費時費力的事情,本篇是研究了三個週末四五天的時間才獲得的,並且爲了不錯誤不會隨便進行臆測,基本上每一個小點都是實際debug執行和打印console獲得的,通過驗證了才寫出來。可是因爲看的深度有限和理解誤差,可能會有一些不全面的地方甚至錯誤,可是從註釋能夠看到有些地方爲何有這個判斷條件即便是源碼的維護者也不太肯定。本篇解讀儘量地實事求事。

相關文章
相關標籤/搜索