1. 程式人生 > >從chrome原始碼看瀏覽器如何載入資源

從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的資源