从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下载和解析完,它能算是正常加载,否则还是推测加载,因此它的优先级没有得到提高。

转载至

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值