參考文檔為google developers中web性能部分的加載性能章節(jié)琼蚯。大家可以不看這篇文章酬凳,直接去看官網(wǎng)文檔。
官網(wǎng)地址
本文為一些總結(jié)和個(gè)人觀點(diǎn)遭庶。
目的
目的當(dāng)然是快宁仔。對(duì)于快,官方也給予了定義(RAIL)峦睡,滿足這個(gè)要求即可翎苫,避免過(guò)度優(yōu)化。
RAIL 是一種以用戶為中心的性能模型榨了。是Response煎谍、Animation、Idle龙屉、Load四個(gè)單詞的縮寫(xiě)呐粘。
- 以用戶為中心;最終目標(biāo)不是讓您的網(wǎng)站在任何特定設(shè)備上都能運(yùn)行很快,而是使用戶滿意作岖。
- Response 立即響應(yīng)用戶唆垃;在 100 毫秒以內(nèi)確認(rèn)用戶輸入。
- Animation 設(shè)置動(dòng)畫(huà)或滾動(dòng)時(shí)鳍咱,在 10 毫秒以內(nèi)生成幀降盹。
- Idle 最大程度增加主線程的空閑時(shí)間。
- Load 持續(xù)吸引用戶谤辜;在 1000 毫秒以內(nèi)呈現(xiàn)交互內(nèi)容蓄坏。
本文只涉及Load部分:如果網(wǎng)站可以秒開(kāi),那就完美了丑念。
關(guān)鍵渲染路徑 CRP(Critical Rendering Path)
網(wǎng)站加載性能跟這個(gè)概念密切相關(guān)涡戳,這個(gè)概念是指當(dāng)瀏覽器接收到html后,到完成第一次繪制到屏幕所需要的步驟脯倚。原文地址
- 構(gòu)建DOM樹(shù)(Constructing the DOM Tree)
- 構(gòu)建CSSOM樹(shù) (Constructing the CSSOM Tree)
- 運(yùn)行JS (Running JavaScript)
- 創(chuàng)建渲染樹(shù) (Creating the Render Tree)
- 生成布局 (Generating the Layout)
- 繪制 (Painting)
1. 構(gòu)建DOM樹(shù)
DOM(Document Object Model)樹(shù)包含了頁(yè)面的所有節(jié)點(diǎn)的對(duì)象渔彰。
HTML可以被部分執(zhí)行,意思是不需要完整地構(gòu)建完DOM樹(shù)才展示到頁(yè)面推正,可以一部分一部分展示恍涂。但是,CSS和JS會(huì)阻塞這個(gè)過(guò)程植榕。
2. 構(gòu)建CSSOM樹(shù)
CSSOM(CSS Object Model)是包含了與DOM相關(guān)的樣式對(duì)象再沧。和DOM相似,但是包含了所有節(jié)點(diǎn)的樣式信息尊残,不管是明確聲明的還是繼承的(附:原文沒(méi)提炒瘸,還有瀏覽器默認(rèn)的)。
CSS是阻塞渲染的資源寝衫,意思是在CSS資源被解析前顷扩,渲染樹(shù)不會(huì)被創(chuàng)建。這是因?yàn)镃SS的級(jí)聯(lián)特性慰毅、后面加載的樣式會(huì)覆蓋前面的隘截。
CSS同樣會(huì)阻塞腳本執(zhí)行,因?yàn)镴S文件必須得等CSSOM構(gòu)建完才能運(yùn)行汹胃。
3. 運(yùn)行JS
JS是阻塞解析(HTML)的資源婶芭,意思是解析HTML文檔會(huì)被JS阻塞。
當(dāng)解析器遇到script標(biāo)簽统台,會(huì)停止解析雕擂,去獲取執(zhí)行JS啡邑。
4. 創(chuàng)建渲染樹(shù)
通過(guò)結(jié)合DOM和CSSOM贱勃,創(chuàng)建最終顯示在頁(yè)面上的渲染樹(shù)(例如display為none的元素,就不會(huì)出現(xiàn)在渲染樹(shù)中)。
5. 生成布局
這個(gè)我沒(méi)看太懂贵扰,講的是viewport大小仇穗,意思是整個(gè)視窗的大小,作為CSS樣式的上下文戚绕。跟平時(shí)講的layout有所不同纹坐。
6. 繪制
將可見(jiàn)元素(附:渲染樹(shù))轉(zhuǎn)換成像素顯示到屏幕上。
繪制的時(shí)間取決于DOM和樣式的復(fù)雜程度舞丛。
文章最后的調(diào)試圖片跟目前Chrome DevTools顯示出入比較大耘子,但是挨個(gè)展開(kāi)event log中的task,基本是一致的球切。
測(cè)試工具
- Lighthouse
- PageSpeed Insights
- WebPageTest
- Pingdom
這幾個(gè)工具都是可以出性能報(bào)告的谷誓。單個(gè)工具都不是十分準(zhǔn)確,詳盡的吨凑,要結(jié)合使用捍歪。
其中Lighthouse可以直接在DevTools中Audits面板中打開(kāi),下圖是報(bào)告(低性能模式)
其中First Meaningful Paint(FMP)和Time to Interactive(TTI)是兩個(gè)很重要的指標(biāo)鸵钝。
下圖是WebPageTest出的報(bào)告:
總結(jié)
為什么總結(jié)要寫(xiě)在這里呢糙臼,因?yàn)槭聼o(wú)巨細(xì),官方文檔寫(xiě)得方方面面涉及太多了恩商。數(shù)了一下变逃,細(xì)節(jié)點(diǎn)大概五六百個(gè),所以先總結(jié)理清一下思路痕届。
文章最后還介紹了webpack韧献,webpack解決了大部分問(wèn)題。
下面就按照這幾個(gè)方面闡述(與官方文檔順序不一樣)研叫。
減少資源體積
文本
JS锤窑、HTML、CSS文件都屬于文本資源嚷炉。
1. 開(kāi)發(fā)和生產(chǎn)代碼分離
現(xiàn)代框架都提供build腳本構(gòu)建生產(chǎn)版本代碼渊啰。
2. 最小化
即刪除注釋、空格申屹、換行等無(wú)用的東西绘证。
3.源碼壓縮
使用UglifyJS壓縮ES5代碼。使用babel-minify或uglify-es壓縮ES2015+代碼哗讥。
4.傳輸壓縮
即采用Gzip壓縮傳輸文本嚷那。Brotli~q11更好。
5.減少庫(kù)的使用
有些庫(kù)很大的杆煞,譬如jQuery魏宽,壓縮后也有100K左右腐泻,如果只用個(gè)選擇器,完全可以用瀏覽器自帶的querySelector等方法队询,或者使用jQuery核心庫(kù)Sizzle派桩。
6.Tree Shaking(刪除未使用代碼)
前提是使用ES6模塊,webpack在打包時(shí)候會(huì)刪除未使用的代碼蚌斩。
然后在引用代碼的時(shí)候铆惑,按需引用。
例如
import * as utils from "../../utils/utils";
改成
import { simpleSort } from "../../utils/utils";
注意:
- babel會(huì)將ES6模塊轉(zhuǎn)成CommonJS模塊送膳,要關(guān)閉此功能员魏。
{
"presets": [
["env", {
"modules": false
}]
]
}
特殊情況1(例lodash)
有些包不是按照ES6模塊開(kāi)發(fā)的,按照上面格式引用依然無(wú)用叠聋,譬如lodash
// This still pulls in all of lodash even if everything is configured right.
import { sortBy } from "lodash";
要解決此問(wèn)題逆趋,有兩種方式
- 改用
lodash-es
庫(kù)
// This will only pull in the sortBy routine.
import sortBy from "lodash-es/sortBy";
- 安裝
babel-plugin-lodash
插件
特殊情況2 (例moment.js)
moment庫(kù)壓縮后有223KB大小,其中170KB是語(yǔ)言包晒奕,不優(yōu)化的話闻书,會(huì)全部被打包到生產(chǎn)環(huán)境代碼去。
要解決此問(wèn)題脑慧,使用moment-locales-webpack-plugin
插件魄眉。
其他特殊情況
https://github.com/GoogleChromeLabs/webpack-libs-optimizations
列舉了很多特殊情況(包括上面兩種)
圖片、視頻
優(yōu)化
一堆專業(yè)性的壓縮圖片技術(shù)和工具就不講了闷袒,就是減少圖片和視頻體積(在不影響網(wǎng)頁(yè)觀看效果前提下)坑律。
- 選擇合適圖片格式(例:從png 232K 到 jpg 42K)
- 刪除元數(shù)據(jù)(即一些“無(wú)用信息”:相機(jī)、拍照日期囊骤、位置等晃择,例:363K刪到325K)
- 裁剪圖片尺寸;提供展示縮略圖也物;降低圖片質(zhì)量(227K到20K)
- 壓縮圖片(551K到133K)
gif動(dòng)畫(huà)轉(zhuǎn)視頻
gif動(dòng)畫(huà)非常大的宫屠,可以利用工具轉(zhuǎn)換成視頻,并設(shè)置自動(dòng)播放滑蚯。
例:將一個(gè)13.7MB的gif動(dòng)畫(huà)轉(zhuǎn)換成了867K的mp4視頻浪蹂。
webm更好,但沒(méi)有mp4兼容性好告材。
<video autoplay loop muted playsinline>
<source src="oneDoesNotSimply.webm" type="video/webm">
<source src="oneDoesNotSimply.mp4" type="video/mp4">
</video>
根據(jù)設(shè)備信息請(qǐng)求不同圖片
srcset坤次,和媒體查詢類似,不同設(shè)備請(qǐng)求不同質(zhì)量圖片
<picture>
<source media="(min-width: 800px)" srcset="head.jpg, head-2x.jpg 2x">
<source media="(min-width: 450px)" srcset="head-small.jpg, head-small-2x.jpg 2x">
<img src="head-fb.jpg" srcset="head-fb-2x.jpg 2x" alt="a head carved out of wood">
</picture>
圖像CDN
專業(yè)圖像CDN站會(huì)幫你自動(dòng)處理好圖片斥赋,但收費(fèi)缰猴。
字體
字體的優(yōu)化也講了很多,有格式問(wèn)題疤剑,有Gzip壓縮問(wèn)題滑绒。
值得注意的是 unicode-range 描述符胰舆,可以指定加載部分子集(不知道要不要服務(wù)器支持)。
例如蹬挤,您可以將 Awesome Font 系列拆分成拉丁文和日文子集,其中的每個(gè)子集將由瀏覽器根據(jù)需要下載:
@font-face {
font-family:'Awesome Font';
font-style: normal;
font-weight: 400;
src: local('Awesome Font'),
url('/fonts/awesome-l.woff2') format('woff2'),
url('/fonts/awesome-l.woff') format('woff'),
url('/fonts/awesome-l.ttf') format('truetype'),
url('/fonts/awesome-l.eot') format('embedded-opentype');
unicode-range:U+000-5FF; /* Latin glyphs */
}
@font-face {
font-family:'Awesome Font';
font-style: normal;
font-weight: 400;
src: local('Awesome Font'),
url('/fonts/awesome-jp.woff2') format('woff2'),
url('/fonts/awesome-jp.woff') format('woff'),
url('/fonts/awesome-jp.ttf') format('truetype'),
url('/fonts/awesome-jp.eot') format('embedded-opentype');
unicode-range:U+3000-9FFF, U+ff??; /* Japanese glyphs */
}
減少請(qǐng)求次數(shù)
這里的減少請(qǐng)求次數(shù)是指減少網(wǎng)絡(luò)請(qǐng)求次數(shù)棘幸,所以緩存也算在這個(gè)優(yōu)化里焰扳。
原因
1. 一個(gè)請(qǐng)求的生命周期
可以看到,一個(gè)請(qǐng)求要經(jīng)過(guò)隊(duì)列等待误续、DNS查找吨悍、初始化連接、SSL蹋嵌、發(fā)送請(qǐng)求育瓜、等待、下載等過(guò)程栽烂。
每一步都是需要時(shí)間和資源的躏仇。
2. Queueing(隊(duì)列)
- 存在更高優(yōu)先級(jí)的請(qǐng)求
- 此源已打開(kāi)六個(gè) TCP 連接,達(dá)到限值腺办。 僅適用于 HTTP/1.0 和 HTTP/1.1
-
瀏覽器正在短暫分配磁盤(pán)緩存中的空間
這里的優(yōu)先級(jí)需要講一下焰手,瀏覽器擁有自己的啟發(fā)式算法,自動(dòng)為各個(gè)資源評(píng)級(jí)怀喉,等級(jí)高的會(huì)優(yōu)先去獲取資源书妻,低的會(huì)等待」#看下圖Priority列
瀏覽器用自己的啟發(fā)式算法為各種資源分配了默認(rèn)級(jí)別:
- 給了html highest級(jí)別躲履,這是肯定的
- 給了css文件highest級(jí)別,這是因?yàn)閏ss會(huì)阻礙渲染(后面會(huì)講)
- 然后給了js文件high級(jí)別(圖中有一些low的js文件是懶加載的)
- 其他一些不重要的資源例如圖片都給了low級(jí)別
3. 網(wǎng)絡(luò)連接
http連接是基于TCP的聊闯,關(guān)于http和TCP及網(wǎng)絡(luò)帶寬工猜、延遲對(duì)加載網(wǎng)頁(yè)影響,可以看下面這篇文章菱蔬。
Primer on Web Performance
我的個(gè)人理解是域慷,tcp連接是一個(gè)復(fù)雜的過(guò)程,要消耗性能和資源汗销,而http/1的底層設(shè)計(jì)更是讓這個(gè)過(guò)程很耗時(shí)犹褒,多個(gè)請(qǐng)求要建立多次tcp連接(關(guān)于http/2是如何優(yōu)化的后面會(huì)講)。而且建立一個(gè)tcp連接后弛针,會(huì)根據(jù)資源的大小叠骑,往返多次。
4. 綜上所述
請(qǐng)求次數(shù)越少越好削茁,力求主頁(yè)加載的都是自己所需要的資源宙枷。
合并文本資源
就是將多個(gè)css文件合并成一個(gè)掉房,多個(gè)js文件合并成一個(gè),webpack打包工具就可以做到慰丛。
合并圖片
即將多張背景圖片合并成一張卓囚,然后通過(guò)背景圖片位置顯示不同圖片。
JS位置
關(guān)于如何加載JavaScript诅病,又是一個(gè)復(fù)雜的哪亿,歷史性的問(wèn)題,可以參照這篇文章Deep dive into the murky waters of script loading贤笆。
其復(fù)雜性在于蝇棉,要想達(dá)到如下目的,很麻煩芥永。
- 不阻塞渲染
- 不導(dǎo)致重復(fù)下載
- 兼容各瀏覽器
簡(jiǎn)單的解決方案是
1. 將script引用標(biāo)簽放在body底部
2. 如果有js腳本必須先執(zhí)行且代碼較少篡殷,可以直接硬寫(xiě)到html中,這樣可以減少請(qǐng)求次數(shù)
緩存
- Memory Cache
- Service Worker Cache
- HTTP Cache (Disk Cache)
- Push Cache
一個(gè)請(qǐng)求會(huì)按順序讀取這幾層緩存埋涧,一旦命中就返回資源板辽。
- Memory Cache
不是我們可以控制的,瀏覽器會(huì)緩存一些圖片等資源到內(nèi)存中棘催,再次遇到請(qǐng)求直接返回戳气,一個(gè)例外是preload的資源也會(huì)放在Memory Cache中。 - Service Worker Cache
可以說(shuō)給我們開(kāi)了一個(gè)口子巧鸭,去定制緩存的行為瓶您,完成一些用http緩存完全做不到的事情。Service Worker完全沒(méi)有任何規(guī)則纲仍,就看我們?cè)趺慈ゾ幋a控制了呀袱,自由度極高,緩存的地方也自由:LocalStorage郑叠、IndexedDB夜赵、FilesSystem、Caches Api乡革。每個(gè)存儲(chǔ)類型都有各自的特點(diǎn)及使用場(chǎng)景寇僧。
官方列舉了一些建議規(guī)則及實(shí)現(xiàn)代碼及其使用場(chǎng)景- Cache only (僅緩存)
- Network only(僅網(wǎng)絡(luò))
- Cache,falling back to network(優(yōu)先緩存,網(wǎng)絡(luò)備用)
- Cache & network race(緩存和網(wǎng)絡(luò)競(jìng)賽)
- Network falling back to cache(優(yōu)先網(wǎng)絡(luò)沸版,緩存?zhèn)溆茫?/li>
- Cache then network(優(yōu)先緩存嘁傀,同時(shí)去請(qǐng)求網(wǎng)絡(luò)(下次請(qǐng)求可用))
- Generic fallback(常規(guī)回退,譬如自定義一個(gè)字符串)
- ServiceWorker-side templating(模板)
- HTTP Cahce(Disk Cache)
很古老的東西了 - Push Cache
這是結(jié)合HTTP/2協(xié)議的緩存视粮,簡(jiǎn)言之就是细办,請(qǐng)求index.html時(shí)候,服務(wù)器不僅返回了index.html蕾殴,也推送了其他關(guān)鍵資源笑撞,這些資源被放在Push Cache中岛啸。
HTTP/2
HTTP/2協(xié)議是好東西,如果有條件使用一定要使用茴肥,以二進(jìn)制分幀為核心從底層解決了HTTP/1的一些令人詬病的問(wèn)題坚踩。使用了HTTP2,上面的一些優(yōu)化手段可以舍棄了(主要是代碼合并方面的瓤狐,例如級(jí)聯(lián)文件瞬铸、圖片精靈)
1. 連接復(fù)用
在 HTTP/1.x 中,如果客戶端要想發(fā)起多個(gè)并行請(qǐng)求以提升性能芬首,則必須使用多個(gè) TCP 連接,不僅耗性能逼裆,浪費(fèi)資源郁稍,而且會(huì)造成隊(duì)首阻塞,效率低下胜宇。
HTTP/2 利用二進(jìn)制分幀層解決了這個(gè)問(wèn)題耀怜,可以在一個(gè)TCP連接中并行發(fā)送多個(gè)請(qǐng)求和響應(yīng),應(yīng)用速度更快桐愉、開(kāi)發(fā)更簡(jiǎn)單财破、部署成本更低。
2. 標(biāo)頭壓縮
HTTP/2 使用HPACK壓縮請(qǐng)求和響應(yīng)標(biāo)頭(header)从诲,極大減少了標(biāo)頭大凶罅 (減小85%-88%)。
我們發(fā)現(xiàn)系洛,僅僅由于標(biāo)頭壓縮俊性,頁(yè)面加載時(shí)間就減少了 45 - 1142 毫秒
3. 服務(wù)器推送
一般打開(kāi)一個(gè)網(wǎng)站,是先向服務(wù)器請(qǐng)求一個(gè)index.html文件描扯,然后解析html定页,再去加載js、css等資源绽诚,這是兩個(gè)串行過(guò)程典徊。
而HTTP/2提供了服務(wù)器推送技術(shù),打開(kāi)一個(gè)網(wǎng)站恩够,服務(wù)器知道你需要什么資源卒落,一起把index.html及js、css等靜態(tài)資源全部推送給你蜂桶,當(dāng)瀏覽器解析完html导绷,請(qǐng)求js和css時(shí)候,直接在緩存中取就可以了屎飘。減少了一次網(wǎng)絡(luò)請(qǐng)求妥曲。
有好處就有弊端:處理的不好的話贾费,重復(fù)發(fā)送資源,浪費(fèi)了緩存這一功能檐盟。
優(yōu)化關(guān)鍵渲染路徑
關(guān)鍵渲染路徑 CRP(Critical Rendering Path)的概念在最開(kāi)始就講了褂萧。
下面說(shuō)一下各個(gè)過(guò)程簡(jiǎn)介及調(diào)試方法(因官網(wǎng)的配圖都比較老了)。
文件列表
index.html
<!DOCTYPE html>
<html>
<head>
<title>Understanding the Critical Rendering Path</title>
<link rel="stylesheet" href="main.css">
</head>
<body>
<header>
<h1>Understanding the Critical Rendering Path</h1>
</header>
<main>
<h2>Introduction</h2>
<p>Lorem ipsum dolor sit amet</p>
<div>
<img src="./images/panda.jpeg" />
</div>
</main>
<footer>
<small>Copyright 2017</small>
</footer>
<script src="main.js"></script>
</body>
</html>
main.css
body { font-size: 18px; }
header { color: plum; }
h1 { font-size: 28px; }
main { color: firebrick; }
h2 { font-size: 20px; }
footer { display: none; }
main.js
function abc() {
console.log("just a simple js");
}
abc();
panda.jpeg
1.構(gòu)建對(duì)象模型(DOM)
Bytes -> Characters -> Tokens -> Nodes -> DOM
四個(gè)過(guò)程
- 根據(jù)編碼將字節(jié)轉(zhuǎn)換成字符串
- 令牌化:將字符串轉(zhuǎn)換成各個(gè)html標(biāo)簽
- 詞法分析:將標(biāo)簽轉(zhuǎn)換成節(jié)點(diǎn)對(duì)象(包含屬性和規(guī)則)
- 構(gòu)建DOM樹(shù)
調(diào)試查看(找到Parse HTML)
打開(kāi)Chrome DevTools -> Audits,評(píng)估完點(diǎn)擊 View Trace葵萎,就會(huì)進(jìn)入Perfermance面板导犹。
主線程Main中的火焰圖和Event Log中是一一對(duì)應(yīng)的。
我們一個(gè)一個(gè)展開(kāi)EventLog中的Task羡忘,找到Parse HTML谎痢,看到解析HTML,構(gòu)建DOM樹(shù)卷雕,話費(fèi)了1.1ms节猿。
提示:totaltime是這一過(guò)程總時(shí)間,selftime是自身執(zhí)行時(shí)間漫雕,totaltime - selftime就是其調(diào)用的子任務(wù)的執(zhí)行時(shí)間滨嘱。
2. 構(gòu)建CSS對(duì)象模型(CSSOM)
Bytes -> Characters -> Tokens -> Nodes -> DOM
與構(gòu)建DOM類似,不過(guò)產(chǎn)出是CSSOM浸间。
小細(xì)節(jié):瀏覽器會(huì)有一組默認(rèn)樣式(User Agent)太雨,也可以說(shuō)會(huì)有一個(gè)默認(rèn)CSSOM樹(shù),解析樣式其實(shí)是去替換默認(rèn)樣式魁蒜。
調(diào)試查看(找到Parse Stylesheet)
這一個(gè)我調(diào)試驗(yàn)證了很久囊扳,我覺(jué)得官網(wǎng)說(shuō)的是對(duì)應(yīng)DevTools中Recalculate Style過(guò)程,并不對(duì)兜看,應(yīng)該是Parse Stylesheet宪拥,理由如下:
- CSSOM構(gòu)建會(huì)阻塞js執(zhí)行,main.js必須等到CSSOM構(gòu)建好铣减,才能執(zhí)行她君。這一點(diǎn)在Recalculate Style上并不能體現(xiàn),反而在Parse Stylesheet上得到了證明:main.js百分百在Parse Stylesheet后執(zhí)行
- DOM解析構(gòu)建對(duì)應(yīng)DevTools中的Parse HTML葫哗,是藍(lán)色的缔刹。Parse Stylesheet也是藍(lán)色的,有理由相信就是CSSOM的解析構(gòu)建劣针。
官方上這樣寫(xiě)校镐,可能是之前的DevTools并沒(méi)有如此細(xì)分。網(wǎng)上查不到相關(guān)信息捺典,所以個(gè)人理解可能也不對(duì)鸟廓。
3. 構(gòu)建渲染樹(shù)(Render tree)
前面根據(jù)HTML構(gòu)建了完整的DOM樹(shù)、根據(jù)CSS構(gòu)建了完整的CSSOM樹(shù)。
這是兩個(gè)獨(dú)立的樹(shù)引谜,根據(jù)這兩個(gè)樹(shù)牍陌,合并計(jì)算出渲染樹(shù)。
這個(gè)過(guò)程大概是這樣:
- 遍歷DOM樹(shù)中可見(jiàn)節(jié)點(diǎn)(不可見(jiàn)節(jié)點(diǎn)有:script员咽、meta標(biāo)簽等毒涧;display:none的元素等,注意visibiility:hidden的不同贝室,會(huì)依然占據(jù)布局)
- 對(duì)每個(gè)可見(jiàn)節(jié)點(diǎn)契讲,找到匹配的CSSOM規(guī)則并應(yīng)用。
- 發(fā)射可見(jiàn)節(jié)點(diǎn)滑频,連同其內(nèi)容和計(jì)算的樣式(我的理解就是發(fā)射渲染樹(shù))
調(diào)試查看(找到Recalculate Style)
同上捡偏,跟官網(wǎng)有出入。而且我認(rèn)為構(gòu)建渲染樹(shù)對(duì)應(yīng)Recalculate Style理由如下:
-
如果在main.js中調(diào)用getComputedStyle獲取樣式峡迷,會(huì)強(qiáng)制提前執(zhí)行Recalculate Style過(guò)程银伟,所以Recalculate Style應(yīng)該就是構(gòu)建渲染樹(shù)。
Recalculate Style還列出了相關(guān)信息:上圖是影響了8個(gè)元素
4. 布局(layout或者叫reflow)
根據(jù)渲染樹(shù)和視口(viewport)凉当,計(jì)算所有可見(jiàn)元素相對(duì)視口的絕對(duì)像素位置和像素尺寸枣申,輸出盒模型(box model)售葡。
調(diào)試查看(找到Layout)
Layout也列出了相關(guān)信息:上圖是需要布局11個(gè)元素
5. 繪制(painting或者叫rasterizing)
就是輸出到屏幕了
調(diào)試查看(找到Update Layer Tree看杭、Paint、Composite Layers挟伙、Rasterize Paint)
- 其中Update Layer Tree我也不知道是歸于Layout還是歸于painting好楼雹。
- 拿剪貼畫(huà)為例,要做一個(gè)三層的剪貼畫(huà)尖阔,先在一張紙上畫(huà)上草地贮缅,然后剪一個(gè)小人并畫(huà)上衣服,然后剪一個(gè)鼻子涂上紅色介却,最后一個(gè)個(gè)貼上去谴供。Paint就是畫(huà)畫(huà)的過(guò)程,Composite Layers就是貼的過(guò)程齿坷,Rasterize Paint就是最后成果圖輸出到屏幕了桂肌。(剪多大,貼在哪兒就是前面Layout的過(guò)程了)
- 注意這里的Rasterize Paint已經(jīng)不再主線程(Main)里了永淌。
優(yōu)化關(guān)鍵路徑
優(yōu)化關(guān)鍵路徑就是縮短1-5消耗的總時(shí)間谭跨。盡快將網(wǎng)頁(yè)渲染到屏幕上。還能縮短首次渲染后屏幕刷新的事件,為交互式內(nèi)容實(shí)現(xiàn)更高刷新率(不懂)螃宙。
CSS會(huì)阻塞渲染
由上可知蛮瞄,同時(shí)具有 DOM 和 CSSOM 才能構(gòu)建渲染樹(shù),所以要精簡(jiǎn)CSS污呼,并盡快提供裕坊。而且要用好媒體查詢,例如指定打印的css文件就不會(huì)阻塞渲染燕酷。
驗(yàn)證
為了驗(yàn)證CSS會(huì)阻塞渲染籍凝,需要讓css文件延遲響應(yīng),我搭建了一個(gè)Go服務(wù)苗缩,延遲2秒返回css文件饵蒂。
wiki.go
package main
import (
"fmt"
"io/ioutil"
"net/http"
"path/filepath"
"time"
)
func main() {
// 延遲css文件
http.HandleFunc("/main.css", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2000 * time.Millisecond)
// fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
absPath, _ := filepath.Abs("./static/main.css")
maincss, _ := ioutil.ReadFile(absPath)
w.Header().Set("Content-Type", "text/css")
fmt.Fprintf(w, string(maincss))
})
// 其他文件直接讀取
http.Handle("/", http.FileServer(http.Dir("static/")))
http.ListenAndServe(":80", nil)
}
index.html
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="main.css" rel="stylesheet">
<title>Critical Path: Script</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<footer>footer</footer>
</body>
</html>
新打開(kāi)一個(gè)瀏覽器標(biāo)簽頁(yè),打開(kāi)localhost酱讶,會(huì)發(fā)現(xiàn)頁(yè)面空白2秒后才顯示內(nèi)容退盯。
打開(kāi)performance調(diào)試,會(huì)發(fā)現(xiàn)有2秒的空白時(shí)間在等待css文件泻肯,然后才進(jìn)行執(zhí)行腳本渊迁,頁(yè)面渲染。
使用JS添加交互
引用:JavaScript 允許我們修改網(wǎng)頁(yè)的方方面面:內(nèi)容灶挟、樣式以及它如何響應(yīng)用戶交互琉朽。 不過(guò),JavaScript 也會(huì)阻止 DOM 構(gòu)建和延緩網(wǎng)頁(yè)渲染稚铣。 為了實(shí)現(xiàn)最佳性能箱叁,可以讓您的 JavaScript 異步執(zhí)行,并去除關(guān)鍵渲染路徑中任何不必要的 JavaScript惕医。
- JavaScript 可以查詢和修改 DOM 與 CSSOM
- JavaScript 執(zhí)行會(huì)阻止 CSSOM
- 除非將 JavaScript 顯式聲明為異步耕漱,否則它會(huì)阻止構(gòu)建 DOM
1. JavaScript 可以查詢和修改 DOM 與 CSSOM
這一條不用講什么了
2. JavaScript 執(zhí)行會(huì)阻塞 CSSOM
這一句我驗(yàn)證時(shí)候繞了很多彎彎,開(kāi)始我猜測(cè)是JavaScript的加載執(zhí)行結(jié)束抬伺,才會(huì)進(jìn)行CSSOM構(gòu)建螟够,多方驗(yàn)證失敗(在服務(wù)端延遲2秒返回js文件峡钓,瀏覽器會(huì)先渲染js之前的妓笙,加載執(zhí)行js后進(jìn)行第二次渲染)。后來(lái)感覺(jué)作者的意思應(yīng)該很直白椒楣,就是:JavaScript執(zhí)行和CSSOM都是在主線程執(zhí)行的给郊,JavaScript執(zhí)行時(shí)候,CSSOM構(gòu)建肯定不能執(zhí)行捧灰,就這么簡(jiǎn)單淆九。
index.html
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="main.css" rel="stylesheet">
<title>Critical Path: Script</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<script>
var str = "";
for(let i=0; i < 10000000; i++) {
str += "a";
}
</script>
<footer>footer</footer>
</body>
</html>
可以看到统锤,我寫(xiě)了一個(gè)執(zhí)行長(zhǎng)達(dá)941ms的腳本,在這段腳本執(zhí)行結(jié)束后炭庙,才進(jìn)行了CSSOM構(gòu)建饲窿。
3. 除非將 JavaScript 顯式聲明為異步,否則它會(huì)阻止構(gòu)建 DOM焕蹄。
這個(gè)意思是逾雄,html中遇到j(luò)s,將會(huì)停止解析DOM,等待js下載執(zhí)行完畢繼續(xù)解析后面的DOM。
在Go服務(wù)器中將main.js延遲20ms返回状共,查看performance镇草。
index.html
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="main.css" rel="stylesheet">
<title>Critical Path: Script</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<script src="main.js"></script>
<footer>footer</footer>
</body>
</html>
可以很明顯看到html被分成2段執(zhí)行了,js后面的頁(yè)面等待js加載執(zhí)行完畢才解析。
將<script src="main.js"></script>
改成<script src="main.js" async></script>
后
加上async后钾麸,瀏覽器不等待js文件加載執(zhí)行,就解析完了dom炕桨。
4. JavaScript 執(zhí)行將暫停饭尝,直至 CSSOM 就緒
參照上面的CSS會(huì)阻塞渲染
配置,JS會(huì)等待CSS加載解析完畢献宫,才會(huì)執(zhí)行钥平。
同樣在Go服務(wù)器將css延遲2秒返回
這里講幾個(gè)注意點(diǎn)
- 4和5肯定是在6之前執(zhí)行的。6和8其實(shí)并不一定嚴(yán)格按順序執(zhí)行遵蚜,如果你在6里讀取了元素樣式帖池,那么8就會(huì)在6之前執(zhí)行奈惑,因?yàn)闉g覽器必須計(jì)算完樣式(Recalculate Stylesheet)吭净,js才能獲取正確的樣式
例如在main.js
中加段代碼:
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
var spanStyle = getComputedStyle(span, null);
console.log(spanStyle.display);
可以看到肴甸,因?yàn)閖s讀取了樣式寂殉,導(dǎo)致了瀏覽器立即計(jì)算樣式返回給js。
這個(gè)在網(wǎng)站運(yùn)行性能里有詳細(xì)講解原在,并且如果代碼寫(xiě)得不好友扰,會(huì)導(dǎo)致反復(fù)計(jì)算樣式,性能奇差庶柿。
- 接上條村怪,如果讀取了元素的尺寸相關(guān),還會(huì)觸發(fā)布局(Layout)
加上代碼,讀取了元素高度
var span = document.getElementsByTagName('span')[0];
console.log(span.clientHeight);
- style和getComputedStyle的區(qū)別
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
console.log(span.style.display)
這段代碼并不會(huì)觸發(fā)瀏覽器計(jì)算樣式浮庐,猜測(cè)是因?yàn)檫@樣寫(xiě)(這個(gè)屬性)是讀取DOM屬性甚负。
5. 將js聲明成異步(async)
<script src="main.js" async></script>
這樣就不會(huì)阻塞構(gòu)建DOM了,不過(guò)不能保證js的執(zhí)行順序,而且各個(gè)瀏覽器實(shí)現(xiàn)的不一致梭域。
延遲加載(懶加載)斑举、預(yù)加載
大概分了幾種情況
- 針對(duì)圖片視頻,可見(jiàn)范圍外的不加載
- 代碼拆分病涨,首屏只加載必須的資源富玷,發(fā)生路由跳轉(zhuǎn)時(shí)候加載其他資源
- 首屏加載必須的資源后,主動(dòng)預(yù)加載一些其他資源既穆,優(yōu)化后續(xù)體驗(yàn)
1. 延遲加載(懶加載)圖片赎懦、視頻
初始只加載屏幕內(nèi)的圖片、視頻幻工,其他用占位圖片代替铲敛。待用戶操作(滾動(dòng)頁(yè)面)到相關(guān)位置,再去獲取圖片会钝。
實(shí)現(xiàn)方式
a. 使用Intersection Observer(最優(yōu))
b. 使用Intersection Observer polyfill
npm install intersection-observer
c. 使用事件處理程序(兼容性最好的方法)
利用scroll伐蒋、resize事件;利用getBoundingClientRect方法與orientationchange事件
d. 使用庫(kù)
- lazysizes
- lozad.js
- blazy
- yall.js
-
react-lazyload
各個(gè)庫(kù)自行研究吧迁酸,其中l(wèi)ozad.js是只使用Intersection Observer的庫(kù)先鱼,性能最好,兼容性不好奸鬓。
CSS中的圖片
CSS中的圖片有一個(gè)特點(diǎn):瀏覽會(huì)計(jì)算需要哪些背景圖焙畔,才會(huì)去加載,所以可以生命兩種樣式串远,設(shè)置不同的背景圖片宏多,通過(guò)切換樣式來(lái)達(dá)到延遲加載
視頻
視頻需要設(shè)定preload="none"
屬性阻止加載視頻資源,設(shè)定poster="one-does-not-simply-placeholder.jpg"
屬性來(lái)指定占位圖片澡罚。
視頻自動(dòng)播放
延遲加載視頻伸但,又要像gif那樣自動(dòng)播放。
設(shè)定autoplay后留搔,通過(guò)poster屬性設(shè)定占位符更胖,data-src設(shè)定真實(shí)資源,需要時(shí)候?qū)rc改成data-src隔显。
JavaScript 代碼拆分和延遲加載
這主要牽扯到工程化却妨,一般工程化會(huì)把單頁(yè)面應(yīng)用打包成一個(gè)js文件,這樣首屏消耗是很高的括眠,可以利用代碼拆分讓首屏只加載必須的資源彪标。
- 動(dòng)態(tài)import
onShowCommentsClick(() => {
import('./comments').then((comments) => {
comments.renderComments();
});
});
這樣Webpack就會(huì)將comments組件單獨(dú)打包,首屏不會(huì)加載掷豺,交互時(shí)候才加載
- 單頁(yè)面應(yīng)用:按路由拆分代碼
跟動(dòng)態(tài)import類似捞烟,不過(guò)是路由級(jí)別的拆分 - 多頁(yè)面應(yīng)用
按照Webpack配置不同的入口文件
// webpack.config.js
module.exports = {
entry: {
home: './src/Home/index.js',
article: './src/Article/index.js',
profile: './src/Profile/index.js'
},
};
3. 預(yù)加載
這主要是優(yōu)化交互體驗(yàn)了账锹,跟首屏關(guān)系不大,預(yù)先加載一些即將使用的資源(譬如代碼拆分后的首屏外的其他資源)坷襟,后續(xù)操作就不會(huì)卡頓了奸柬。
資源優(yōu)先級(jí)
上面講隊(duì)列的時(shí)候稍微提到了下。
瀏覽器加載資源會(huì)有不同優(yōu)先級(jí)婴程,查看方法:DevTools -> Network廓奕,右擊表格的標(biāo)題欄,勾選Priority選項(xiàng):
瀏覽器會(huì)采用啟發(fā)式算法档叔,對(duì)不同資源有不同的優(yōu)先級(jí)桌粉。因?yàn)榫W(wǎng)絡(luò)連接是有限的,所以會(huì)為優(yōu)先級(jí)高的資源請(qǐng)求衙四,剩下的排隊(duì)等待铃肯。
可以通過(guò)如下方式改變一些行為:
1. 預(yù)加載 preload
<link rel="preload" as="script" href="super-important.js">
<link rel="preload" as="style" href="critical.css">
preload可以提高資源的優(yōu)先級(jí),使用場(chǎng)景有:字體的加載優(yōu)先級(jí)是很低的传蹈,可以通過(guò)preload來(lái)提高押逼;preload可以加載CSS或者JS中定義使用的資源;html中引用css會(huì)阻礙渲染惦界,可以通過(guò)preload來(lái)避免挑格;
2. 預(yù)連接 preconnect
<link rel="preconnect" >
一個(gè)請(qǐng)求要經(jīng)過(guò)很多步驟,preconnect可以預(yù)先進(jìn)行一些步驟:DNS 解析沾歪,TLS 協(xié)商漂彤,TCP 握手。大大改善了一些情況下的延遲問(wèn)題灾搏。
也可以使用dns-prefetch挫望,僅僅進(jìn)行DNS查詢。
3. 預(yù)提取 prefetch
這個(gè)跟preload很容易弄混淆狂窑,prefetch是去預(yù)提取一些資源到緩存中媳板,以后會(huì)用到。
4. 預(yù)渲染 prerender
和prefetch相似蕾域,但是prerender會(huì)在后臺(tái)渲染頁(yè)面拷肌。(不知道怎么測(cè)試到旦,我測(cè)試了好像沒(méi)什么效果)
頁(yè)面繪制
這一部分不單單是針對(duì)加載頁(yè)面了旨巷,關(guān)于js執(zhí)行效率和頁(yè)面渲染效率等,打算另開(kāi)一個(gè)文章寫(xiě)添忘。
其他
性能API
Navigation Timing和Resource Timing幫我們收集性能相關(guān)信息采呐。
使用這些API有如下特點(diǎn)
- 數(shù)據(jù)化:不用再在DevTools中一點(diǎn)點(diǎn)調(diào)試了,可以直接讀取相關(guān)數(shù)據(jù)搁骑,做出自己的性能分析報(bào)告(很多性能測(cè)試工具就是基于此API)斧吐;
- 更準(zhǔn)確:打開(kāi)DevTools調(diào)試其實(shí)也是消耗性能的又固,加載比直接打開(kāi)網(wǎng)頁(yè)要慢
- 可以收集用戶真實(shí)的數(shù)據(jù):直接在客戶端收集信息后上傳服務(wù)器,統(tǒng)計(jì)分析
Navigation Timing
performance.getEntriesByType("navigation");
導(dǎo)航時(shí)間線收集HTML文檔性能指標(biāo)
Resource Timing
資源時(shí)間線收集了文檔依賴的各種資源的性能指標(biāo)
performance.getEntriesByType("resource");
其他相關(guān)用法
- getEntriesByName:收集某個(gè)資源的性能指標(biāo)
- getEntries:收集所有性能指標(biāo)
- PerformanceObserver:自己循環(huán)遍歷性能指標(biāo)煤率,會(huì)帶來(lái)些問(wèn)題仰冠,可以實(shí)現(xiàn)這個(gè)對(duì)象實(shí)例來(lái)遍歷
- navigator.sendBeacon:如果想將用戶性能指標(biāo)數(shù)據(jù)發(fā)給服務(wù)器,可以寫(xiě)在unload事件中蝶糯,但是一般寫(xiě)法(ajax洋只,fetch等)會(huì)阻塞瀏覽器≈绾矗可以使用navigator.sendBeacon避免這個(gè)問(wèn)題识虚。
新的時(shí)間API:performance.now
相關(guān)文檔
上面的時(shí)間線中的時(shí)間都是通過(guò)這個(gè)方法處理處理的。
背景
之前處理時(shí)間一般都是用Date相關(guān)方法妒茬。有很多缺陷
- 不夠精確担锤,只能到毫秒數(shù)
- 不僅不夠精確,還不準(zhǔn)(受制于系統(tǒng)時(shí)鐘偏移)
例如以前自己用Date獲取時(shí)刻相減少乍钻,提示一段代碼執(zhí)行時(shí)間肛循,發(fā)現(xiàn),有時(shí)候執(zhí)行16ms银择,有時(shí)候執(zhí)行60多ms育拨,顯然是不對(duì)的。(當(dāng)然也不會(huì)偏差的過(guò)分)欢摄。
所以有了新規(guī)范熬丧,目前是High Resolution Time Level 2。
具體實(shí)現(xiàn)就是通過(guò)實(shí)現(xiàn)performance相關(guān)屬性和方法怀挠。
使用
performance.now(); // 7219725.014999989
輸出了一個(gè)相對(duì)于初始時(shí)間的亞毫秒數(shù)(sub-millisecond析蝴,其實(shí)就是高精度的毫秒數(shù))。
初始時(shí)間
可以通過(guò)
performance.timeOrigin // 1558493857670.848
獲取绿淋。這個(gè)屬性值闷畸,可以是瀏覽上下文創(chuàng)建時(shí)間,可以是新文檔對(duì)象創(chuàng)建時(shí)間吞滞,可以是worker創(chuàng)建時(shí)間佑菩。不用關(guān)心初始時(shí)間,只是個(gè)參照而已裁赠。性能分析分析的都是時(shí)間段殿漠。
requestAnimationFrame
以前寫(xiě)動(dòng)畫(huà)都是利用setTimeout來(lái)處理的,有諸多弊端佩捞,性能也不好绞幌。這個(gè)方法可以改善這個(gè)問(wèn)題。