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

對瀏覽器加載資源有很多不確定性, 例如

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

通過源碼可以找到答案。 此次源碼解讀基於Chromium64
下面通過加載資源的步驟, 一次說明。

1. 開始加載

通過以下命令打開chromium, 同時打開一個網頁

chrominm -- renderer-startup-dialog https://www.baidu.com

Chrome會在DocumentLoader.cpp裏通過以下代碼去加載:

main_resource_ = 
		RawResource::FetchMainResource(fetch_params, fetcher(), substitute_data_);

頁面資源屬於MainRescouce, chrome把Rescource歸爲以下幾種:

enum Type : uint8_t {
	kMainResource,
	kImage,
	kCSSStyleSheet,
	kScript,
	kFont,
	kRaw,
	kSVGDocument,
	kXSLStyleSheet,
	kLinkPrefetch,
	kTextTrack,
	kImaportResource,
	kMedia,
	kManifest,
	kMock
}

除了常見的image/css/js/font之外,我們發現還有像textTranck的資源,這個是什麼東西呢?這個是video的字母, 使用webvtt格:

<video controls poster="/images/sample.gif">
	<source src='sample.mp4' type='vide/mp4'>
	<track kind='captions' src='sampleCaption.vtt' srclang='en'>
</video>

還有動態請求ajax屬於Raw類型。 因爲ajax可以請求多種資源。
MainResource包括location即導航輸入地址得到的頁面、使用franme/iframe嵌套的、通過超鏈接點擊的頁面以及表單提交這幾種。

接着交給稍微底層的RescurceFeche去加載, 所有的資源啊都是通過它加載的:

fetcher->RequestResource(
	paras, RawResourceFactory(Resource::kMainResource), substitute-data
)

2. 預處理請求

每個請求會生成一個RescourceRequest對象, 這個對象包含了http請求的所有信息:
在這裏插入圖片描述

包括url, http header、 http body等, 還有請求的優先級信息等:
在這裏插入圖片描述

然後會更具頁面的加載策略對這個請求做一些預處理, 如下代碼:

PrepareRequestResult result = prepareRequest(params, factory, substityt_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* scp = GetContentSecurityPolicy();
if(csp && !csp->AllowRequest(
						request_context, rul, options.content_security_policy_nonce,
						options.integrity_metadata, options.parser_disposition,
						redirect_status, reporting_policy, check_header_type)){
		return ResourceRequesttBlocedReason::KCSP;					
	}

如果有csp並且AllowRequest沒有通過的話就會返回堵塞的原因。 具體的檢查過程是根據不同的資源類型去獲取該類資源的CSP設定進行比較。

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

ModifyRequestForCSP(request);

主要是升級http爲https

(2). upgrade-insecure-request

如果設定了一下csp規則:

<meta http-equiv="Content-Security-Policy" content='upgrad-insecure-request'>

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

url.SetProtocol("https");
if(url.prot()==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_requ3st.GetRedirectStatus(),url, reporting_policy))
	return ResourceRequestBlockedReason::kMixedContent;

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

case WebUrlRequest::kRequestContextAudio:
case WebURLRequest::kRequestContextFavicon:
case WebURLquest::kRequestContextIage:
case WebURLRequest::kREquestContextVideo:
	return WebMixedContextContextType::kOptionallyBlockable;

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

而剩下的其他所有幾乎 都是blockable的, 包括js/css/frame/XMLHttpRequest等:
在這裏插入圖片描述

我們注意到img srcset 裏的資源也是默認會被阻止的, 即下面的img會被block:

<img srcset="http://fedren.com.test-1x.png 1x, htt://fedren.com/text-2x.png 2x /">

3. 資源優先級

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

通過調用一下函數設定:

resource_request.SetPriority(ComputLoadPriority(
	resource_type, params.GetResourceRequest(), ResourcePriority::kNotVisible,
	params.Defer(), params.GetSpeculativePreloadType(),
	params.Isl=LInkPreload()))

我們來看看這個函數裏是怎麼計算當前資源的優先級的。
首先每個資源都有一個默認的優先級, 這個優先級作爲初始值:

ResourceLoadPriority priority = TypeToPriority(type);

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

ResourceLoadPriority TypeToPriority(Resource::Type type) {
	switch(type) {
		case Resource::kMainResource:
		case Resoruce::KCSSStyleSheet;
		case Resource::kFont:
			return kResourceLoadPriorityVeryHigh;
		case Resource::KXSStyleSheet:
			DCHECK(RuntimeEnabledFeatures::XSLTEnabled());
		case Resource::kRaw:
		case Resource::kImportResource:
		case Resource::kScript:
			return kResourceLoadPriortyHigh;
		case Resource::kMainifest::
		case Resource::kMock:
			return kResourceLoadPriorityMedium;
		case Resource::kImage:
		case Resource::kTextTrack:
		case Resource::kMedia:
		case Resource::kSVGDoucment:
			return kResourceLoadPriorityLow;
		case Resource::kLinkPrefetch:
			return kResourceLoadPriorityVeryLow;
	}
	return kResourceLoadPriorityUnresolved;
}

可以看到優先級總共分爲五級: very-high、high、medium、low、very-low,其中MainRescource頁面、css、字體這三個的優先級是最高的,然後是script,ajax這種, 而圖片、音頻的默認優先級是比較低的, 最低的事prefetch預加載的資源。

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

<link rel="prefetch" href="image.png">

瀏覽器空閒的時候就會去加載。另外還可以與解析DNS:

<link rel="preconnect" href="https://cdn.chime.me">

預建立TCP鏈接:

<link rel="preconnect" href="https;??cdn.chime.me">

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

注意上面的switch-case設定資源優先級有一個順序, 如果既是script都是prefetch的話, 得到的優先級是high, 而不是prefetch的事very high, 因爲prefetch是最後一個判斷。 所以在設定了資源默認的優先級之後,會在對一些情況做一些調整, 主要是對prefetch/preload的資源。 包括:

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

如下代碼:

if (type == Resource::kFont&&is_link_preload)
	priority = kResourceLoadPrioritHigh

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

b)降低defer/asyncde script的優先級

如下代碼:

if(type == Resourc::kScript) {
	if(FetchParameters::kLazyLoad == defer_option) {
		priority = kResourceLoadPriorityLow;
	}
}

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

c)頁面底部preload的script優先級變成medium

如下代碼:

if(type ==Resource::kScript) {
	if(FetchP昂讓meters::kLazyLoad == defer_option) {
		priority = kResourceLoadPriorityLow;
	}else if(speculative_preload_type == FetchParameters::SpeculativePreloadType::kInDoucment && image_fetched_) {
		priority=kResourceLoadPriorityMedium;
	}
}

如果是defer的script那麼優先級調成最低(上面第三小點),否則如果是preload的script, 並且如果頁面已經加載了一張圖片就認爲這個script是頁面偏底部的位置, 就把它的優先級調成medium.。 通過一個flag決定是否已經加載過第一張圖片了:

if(type == Resoucre::kImage && !is_link_preload) {
	image_fetched_ = true;
}

資源在第一張非preload的圖片前認爲是early, 而後面認爲是late, late的script的優先級會偏低。

什麼是preload呢? preload不同於prefetch的, 在早期瀏覽器,script資源都是阻塞加載的, 當頁面遇到一個script, 那麼要等這個script下載和執行完了,纔會繼續解析剩下的dom結構, 也就是說sscript是串行加載的, 摒棄會堵塞其他資源的加載,這樣會導致頁面整體的加載速度慢, 所以早在2008年的時候瀏覽器除了一個推測加載策略, 即遇到script的時候, dom會停止構建, 但是會繼續去搜索頁面需要加載的資源, 如看下後續的html有沒有img/script標籤, 先進行預加載, 而不是等到dom的時候纔去架子啊, 這樣大大提高了頁面整體的加載速度。

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

如下代碼:

return stc::max(priority, resource_request.Priority());

如果是同步加載的資源, 那麼它的request對象裏面的優先級是最高的, 所以本來是high的ajax同步請求在最後return的時候會變成very-high.

這裏是取了兩個值的最大值, 第一個值是上面進行各種判斷得到depriority, 第二個在初始這個ResourceRequest對象本身就有的一個優先級屬性,返回最大值後再重新設置resource_request的優先級屬性。

在構建resource request對象時所有的資源都是最低的, 這個可以從構建函數你知道:

ResourceReques::ResoucreReques(Conset KURL& url):url_(url),serviec_worker_mode_(WebURLRequest::ServiceWorkerMode:KAll),priority_(kResourceLoadPriorityLowest)

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

void FetchParameters::MakerSynchronouse(){
	resource_request_.SetPriority(kResourceLoadPriorityHightest);
	resource_request.SetTimeoutInterval(10);
	options_.synchronous_policy = kRequestSynchronously;
}

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

(2)轉換成Net的優先級

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

resource_request->prioirty = ConverWebKitPriorityToNetPriority(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, SchedduledResourceRequest* request) {
	SetRequestAttributes(request, DetermineRequestAttributes(request));
	ShouldStartReqResult should_start = ShouldStartRequest(request);
	if(should_start == START_REQUEST){
		StartRequest(request, STRAT_SYNC, RequestStartTrigger::NONE);
	}eles {
		pending_request_.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, 然後調用shouldRequest判斷是否能運行了, 如果能的話就把它 從pending requests 裏面刪掉, 然後運行。

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

  1. 收到來自渲染線程IPC::Mojo的請求加載資源的消息
  2. 每個請求完成之後, 觸發加載pending request 你還未加載的請求
  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的網絡熟讀調爲fase 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兩種:

statice const net ::RequestPriority
	kDelayablePriorityThreshould = 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的請求。

然後, 上面提到的ShouldStartPequest函數, 這個函數時規劃資源加載順序最重要的函數, 從源碼註釋可以知道它大概的過程:

// 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
  5. 個,否則要進行排隊。

注意這裏說的開始加載,並不是說能夠開始請求建立連接了。 源碼裏面叫
in flight,在飛行中, 而不是叫in request之類的, 能夠進行in filght的請求是指那些不用queue的請求, 如下圖:
在這裏插入圖片描述

白色條是指queue的時間段, 而灰色的事已經in filght了, 但受到同域只能最後只能建立6個tcp鏈接等的影響而進入的stalled狀態, 綠色是ttfb從開始建立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的資源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下載和解析完,它能算是正常加載,否則還是推測加載,因此它的優先級沒有得到提高。

轉載至

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章