[TOC]
## HTML
### AMP HTML
> AMP是什么
Google 前沿的 AMP 「 Accelerated Mobile Pages 」技術,能使用戶從搜索引擎當中進入我們頁面的體驗得到一個極大的提升。確切地說,AMP并不是一門新的技術,它只是一種能讓頁面打開得更快的解決方案。
> 我們為什么選擇AMP
1、AMP能夠帶來SEO排名優化。
2、AMP Cache能夠讓我們充分借助Google CDN Cache的優勢。雖然我們內部已經做了很多優化,包括DNS預熱,但如果能有Google全球CDN支持就更是件好事。
3、Google搜索結果對AMP頁面有預加載處理,能讓用戶更快地到達我們的著陸頁。
~~~
<!doctype html>
<html ?>
<head>
<meta charset="utf-8">
<link rel="canonical" href="hello-world.html">
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<style amp-boilerplate>
body{
-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;
-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;
-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;
animation:-amp-start 8s steps(1,end) 0s 1 normal both}
@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}
@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}
@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}
@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}
@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}
</style>
<noscript>
<style amp-boilerplate>
body{
-webkit-animation:none;
-moz-animation:none;
-ms-animation:none;
animation:none
}
</style>
</noscript>
<script async src="https://cdn.ampproject.org/v0.js"></script>
</head>
<body>Hello World!</body>
</html>
~~~
<br>
### 減少沒有必要的嵌套
~~~
<div class="form">
<form>
...
</form>
</div>
// 可以改寫為
<form class="form">
...
</form>
~~~
<br>
### 減少使用iframe
用iframe可以把一個HTML文檔插入到父文檔里,重要的是明白iframe是如何工作的并高效地使用它。
iframe的優點:
* 引入緩慢的第三方內容,比如標志和廣告
* 安全沙箱
* 并行下載腳本
iframe的缺點:
* 代價高昂,即使是空白的iframe
* 阻塞頁面加載
* 非語義
<br>
### DNS Prefetching
DNS Prefetch是一種DNS預解析技術,當我們瀏覽網頁時,瀏覽器會在加載網頁時對網頁中的域名進行預解析并緩存,這樣在瀏覽器加載網頁中的鏈接時,就無需進行DNS解析,減少用戶的等待時間,提高用戶體驗。DNS Prefetch現已被主流瀏覽器支持,大多數瀏覽器針對DNS解析都進行了優化,典型的一次DNS解析會耗費20~120ms,減少DNS解析時間和次數是個很好的優化措施。這里附上一張Can I use it官網上的DNS Prefetch支持情況圖:

用法
~~~
<link rel="dns-prefetch" href="//tj.koudaitong.com/" />
<link rel="dns-prefetch" href="//imgqn.koudaitong.com/" />
<link rel="dns-prefetch" href="//kdt-static.qiniudn.com/" />
// 強制開啟DNS Prefetching
<meta http-equiv="x-dns-prefetch-control" content="on">
~~~
結論
* 對于引用了大量很多其他域名的資源的網頁會有作用,如果你的網站,基本所有的資源都在你本域名下,那么這個基本沒有什么作用。因為DNS Chrome在訪問你的網站就幫你緩存了。
* 一般情況下所有的a標簽的href都會自動去啟用DNS Prefetching,網頁的a標簽href帶的域名,是不需要在head里面加上link手動設置的
* a標簽的href是可以在chrome、firefox包括高版本的IE,但是在HTTPS下面不起作用,需要meta來強制開啟功能
* 對重定向跳轉的新域名做手動dns prefetching,比如:頁面上有個A域名的鏈接,但訪問A會重定向到B域名的鏈接,這么在當前頁對B域名做手動dns prefetching是有意義的
功能的有效性
* 如果本地就有緩存,那么解析大概是0~1ms,如果去路由器查找大概是15ms,如果當地的服務器,一些常見的域名可能需要150ms左右,那么不常見的可能要1S以上。
* DNS解析的包很小,因為DNS是分層協議的,不需要跟http協議一樣,一個UDP的包就ok,大概100bytes,快速。
* 本機的DNS緩存是有限,例如XP大概50到200個域名,所以Chrome這里做了優化,會根據你的網站訪問頻率,來保證你常用的網站的DNS都能被緩存住。
在chrome的地址欄輸入查看
~~~
"about:histograms/DNS"
"about:dns"
~~~
<br>
### preconnet
瀏覽器要建立一個連接,一般需要經過DNS查找,TCP三次握手和TLS協商(如果是https的話),這些過程都是需要相當的耗時的,所以preconnet,就是一項使瀏覽器能夠預先建立一個連接,等真正需要加載資源的時候就能夠直接請求了。
而一般形式就是
~~~
<link rel="preconnect" href="//example.com">
<link rel="preconnect" href="//cdn.example.com" crossorigin>
~~~
瀏覽器會進行以下步驟:
* 解釋href的屬性值,如果是合法的URL,然后繼續判斷URL的協議是否是http或者https否則就結束處理
* 如果當前頁面host不同于href屬性中的host,crossorigin其實被設置為anonymous(就是不帶cookie了),如果希望帶上cookie等信息可以加上crossorign屬性,corssorign就等同于設置為use-credentials
<br>
### prefetch
能夠讓瀏覽器預加載一個資源(HTML,JS,CSS或者圖片等),可以讓用戶跳轉到其他頁面時,響應速度更快。
一般形式就是
~~~
<link rel="prefetch" href="//example.com/next-page.html" as="html" crossorigin="use-credentials">
<link rel="prefetch" href="/library.js" as="script">
~~~
雖然是預加載了,但是頁面是不會解析或者JS是不會直接執行的。
<br>
### prerender
而prerender不僅會加載資源,還會解執行頁面,進行預渲染,但是這都是根據瀏覽器自身進行判斷。
瀏覽器可能會分配少量資源對頁面進行預渲染掛起部分請求直至頁面可見時可能會放棄預渲染,如果消耗資源過多等等情況。。。
一般形式
~~~
<link rel="prerender" href="//example.com/next-page.html">
~~~
<br>
## CSS
### 把樣式表放在頂部
通常情況下 CSS 被認為是阻塞渲染的資源,在CSSOM 構建完成之前,頁面不會被渲染,放在頂部讓樣式表能夠盡早開始加載。但如果把引入樣式表的 link 放在文檔底部,頁面雖然能立刻呈現出來,但是頁面加載出來的時候會是沒有樣式的,是混亂的。當后來樣式表加載進來后,頁面會立即進行重繪,這也就是通常所說的閃爍了。
<br>
### 內聯首屏關鍵CSS(Critical CSS)
性能優化中有一個重要的指標——首次有效繪制(First Meaningful Paint,簡稱FMP)即指頁面的首要內容(primary content)出現在屏幕上的時間。這一指標影響用戶看到頁面前所需等待的時間,而 **內聯首屏關鍵CSS**能減少這一時間。
<br>
既然內聯CSS能夠使頁面渲染的開始時間提前,那么是否可以內聯所有的CSS呢?答案顯然是否定的,這種方式并不適用于內聯較大的CSS文件。因為[初始擁塞窗口](https://link.juejin.im?target=https%3A%2F%2Ftylercipriani.com%2Fblog%2F2016%2F09%2F25%2Fthe-14kb-in-the-tcp-initial-window%2F)存在限制(TCP相關概念,通常是 14.6kB,壓縮后大小),如果內聯CSS后的文件超出了這一限制,系統就需要在服務器和瀏覽器之間進行更多次的往返,這樣并不能提前頁面渲染時間。因此,我們應當**只將渲染首屏內容所需的關鍵CSS內聯到HTML中**。
<br>
既然已經知道內聯首屏關鍵CSS能夠優化性能了,那下一步就是如何確定首屏關鍵CSS了。顯然,我們不需要手動確定哪些內容是首屏關鍵CSS。Github上有一個項目Critical CSS4,可以將屬于首屏的關鍵樣式提取出來,大家可以看一下該項目,結合自己的構建工具進行使用。當然為了保證正確,大家最好再親自確認下提取出的內容是否有缺失。
<br>
不過內聯CSS有一個缺點,內聯之后的CSS不會進行緩存,每次都會重新下載。不過如上所說,如果我們將內聯后的文件大小控制在了14.6kb以內,這似乎并不是什么大問題。
<br>
### 異步加載CSS
CSS會阻塞渲染,在CSS文件請求、下載、解析完成之前,瀏覽器將不會渲染任何已處理的內容。有時,這種阻塞是必須的,因為我們并不希望在所需的CSS加載之前,瀏覽器就開始渲染頁面。那么將首屏關鍵CSS內聯后,剩余的CSS內容的阻塞渲染就不是必需的了,可以使用外部CSS,并且異步加載。
#### JavaScript動態創建
~~~
// 創建link標簽
const myCSS = document.createElement( "link" );
myCSS.rel = "stylesheet";
myCSS.href = "mystyles.css";
// 插入到header的最后位置
document.head.insertBefore( myCSS, document.head.childNodes[ document.head.childNodes.length - 1 ].nextSibling );
~~~
<br>
#### 將link元素的 media 屬性設置為用戶瀏覽器不匹配的媒體類型
將link元素的`media`屬性設置為用戶瀏覽器不匹配的媒體類型(或媒體查詢),如`media="print"`,甚至可以是完全不存在的類型`media="noexist"`。對瀏覽器來說,如果樣式表不適用于當前媒體類型,其優先級會被放低,會在不阻塞頁面渲染的情況下再進行下載。
當然,這么做只是為了實現CSS的異步加載,別忘了在文件加載完成之后,將`media`的值設為`screen`或`all`,從而讓瀏覽器開始解析CSS。
~~~
<link rel="stylesheet" href="mystyles.css" media="noexist" onload="this.media='all'">
~~~
<br>
#### 將 link 元素標記為 alternate
通過`rel`屬性將`link`元素標記為`alternate`可選樣式表,也能實現瀏覽器異步加載。同樣別忘了加載完成之后,將`rel`改回去。
~~~
<link rel="alternate stylesheet" href="mystyles.css" onload="this.rel='stylesheet'">
~~~
<br>
#### rel="preload"
上述的三種方法都較為古老。現在,[rel="preload"](https://link.juejin.im?target=https%3A%2F%2Fwww.w3.org%2FTR%2Fpreload%2F)這一Web標準指出了如何異步加載資源,包括CSS類資源。
~~~
<link rel="preload" href="mystyles.css" as="style" onload="this.rel='stylesheet'">
~~~
注意,`as`是必須的。忽略`as`屬性,或者錯誤的`as`屬性會使`preload`等同于`XHR`請求,瀏覽器不知道加載的是什么內容,因此此類資源加載優先級會非常低。`as`的可選值可以參考上述標準文檔。
<br>
看起來,`rel="preload"`的用法和上面兩種沒什么區別,都是通過更改某些屬性,使得瀏覽器異步加載CSS文件但不解析,直到加載完成并將修改還原,然后開始解析。
<br>
但是它們之間其實有一個很重要的不同點,那就是**使用preload,比使用不匹配的`media`方法能夠更早地開始加載CSS**。所以盡管這一標準的支持度還不完善,仍建議優先使用該方法。
<br>
### 避免使用CSS表達式
<br>
### 減少選擇器層級
~~~
.wrapper .list .item .success {}
// 可以寫成如下:
.wrapper .list .item-success {}
~~~
<br>
### 關鍵選擇器要盡量特殊
CSS 選擇器在匹配的時候是由右至左進行的,因此最后一個選擇器常被稱為關鍵選擇器,因為最后一個選擇越特殊,需要進行匹配的次數越少。要千萬避免使用 *(通用選擇器)作為關鍵選擇器。因為它能匹配到所有元素,進而倒數第二個選擇器還會和所有元素進行一次匹配。這導致效率很低下。
另外 first-child 這類偽類選擇器也不夠特殊,也要避免將它們作為關鍵選擇器。關鍵選擇器越特殊,瀏覽器就能用較少的匹配次數找到待匹配元素,選擇器性能也就越好。
<br>
### 選擇<link>舍棄@import
前面提到了一個最佳實踐:為了實現逐步渲染,CSS應該放在頂部。
在IE中用@import與在底部用<link>效果一樣,所以最好不要用它。
<br>
### 避免使用濾鏡
IE專有的AlphaImageLoader濾鏡可以用來修復IE7之前的版本中半透明PNG圖片的問題。在圖片加載過程中,這個濾鏡會阻塞渲染,卡住瀏覽器,還會增加內存消耗而且是被應用到每個元素的,而不是每個圖片,所以會存在一大堆問題。
最好的方法是干脆不要用AlphaImageLoader,而優雅地降級到用在IE中支持性很好的PNG8圖片來代替。如果非要用AlphaImageLoader,應該用下劃線hack:_filter來避免影響IE7及更高版本的用戶。
<br>
### 將漸變或者會動畫元素放到單獨的繪制層中
繪制并非在一個單獨的畫布上進行的,而是多層。因此將那些會變動的元素提升至單獨的圖層,可以讓他的改變影響到的元素更少。
可以使用 CSS 中的 will-change: transform; 或者 transform: translateZ(0); 這樣來將元素提升至單獨的圖層中。
在調試的時候你可以在 Chrome DevTools 的 timeline 面板來觀察繪制圖層。當然也不是說圖層越多越好,因為新增加一個圖層可能會耗費額外的內存。且新增加一個圖層的目的是為了避免某個元素的變動影響其他元素。
<br>
### 使用flexbox替代老的布局模型
老的布局模型以相對/絕對/浮動的方式將元素定位到屏幕上
Floxbox布局模型用流式布局的方式將元素定位到屏幕上
通過一個小實驗可以看出兩種布局模型的性能差距,同樣對1300個元素布局,浮動布局耗時14.3ms,Flexbox布局耗時3.5ms。


<br>
### 利用GPU硬件加速瀏覽器渲染
應用動畫效果的元素應該被提升到其自有的渲染層,但不要濫用。
在頁面中創建一個新的渲染層最好的方式就是使用CSS屬性winll-change,對于目前還不支持will-change屬性、但支持創建渲染層的瀏覽器,可以通過3D transform屬性來強制瀏覽器創建一個新的渲染層。需要注意的是,不要創建過多的渲染層,這意味著新的內存分配和更復雜的層管理。
對于我們的瀏覽器而言,拿到我們的html文本串開始按順序解析成DOM樹,并與同步解析出來的CSS匹配生成渲染樹(跟DOM樹的節點不是一一對應,比如display:none的節點就不會插入渲染樹)

瀏覽器將渲染樹的節點用一個圖層表示,這樣層層疊加在一起生成layout,有點像ps的圖層疊加的概念(可以通過火狐瀏覽器開發者工具3維展示更直觀),一般情況下對節點的任何涉及尺寸的改變都會引起layout的重排重繪(重排和重繪是造成瀏覽器渲染的最大性能損耗的因素),但有種開小灶的情況Composite Layers(復合圖層)直接交給我們GPU中單獨的合成器進程處理,自身變化不會引起其他層的位置變化,不會引起重排重繪。tranform 3d 或 winll-change悄悄的告訴我們的瀏覽器把元素解析作為復合圖層交給單獨進程去處理的。
~~~
.moving-elemen {
will-change: transform;
transform: translateZ(0);
}
~~~


<br>
#### 用transform/opacity實現動畫效果
使用transform/opacity實現動畫效果,會跳過渲染流程的布局和繪制環節,只做渲染層的合并。

使用transform/opacity的元素必須獨占一個渲染層,所以必須提升該元素到單獨的渲染層。
<br>
### 減少回流和重繪
當頁面布局和幾何屬性改變時就需要回流。下述情況會發生瀏覽器回流:
* 添加或者刪除可見的DOM元素;
* 元素位置改變;
* 元素尺寸改變(margin、padding、border、width和height)
* 內容改變,尤其是輸入控件
* 頁面渲染初始化;
* 瀏覽器窗口尺寸改變(resize事件發生時);
* 改變文字大小;
* 激活偽類,如:hover;
* offsetWidth, width, clientWidth, scrollTop/scrollHeight的計算, 會使瀏覽器將漸進回流隊列Flush,立即執行回流;
* 設置style屬性;
<br>
減少回流、重繪其實就是需要減少對render tree的操作(合并多次多DOM和樣式的修改),并減少對一些style信息的請求,盡量利用好瀏覽器的優化策略。具體方法有:
#### 直接改變className
如果動態改變樣式,則使用cssText(考慮沒有優化的瀏覽器)
~~~
// 不好的寫法
var changeDiv = document.getElementById('changeDiv');
changeDiv.style.color = '#093';
changeDiv.style.background = '#eee';
changeDiv.style.height = '200px';
// 比較好的寫法
div.changeDiv {
background: #eee;
color: #093;
height: 200px;
}
document.getElementById('changeDiv').className = 'changeDiv';
~~~
#### 讓要操作的元素進行”離線處理”,處理完后一起更新
* 使用DocumentFragment進行緩存操作,引發一次回流和重繪;
* 使用display:none技術(由于display屬性為none的元素不在渲染樹中,對隱藏的元素操作不會引發其他元素的重排。如果要對一個元素進行復雜的操作時,可以先隱藏它,操作完成后再顯示。這樣只在隱藏和顯示時觸發2次重排。);
* 使用cloneNode(true or false) 和 replaceChild 技術,引發一次回流和重繪;
* 將需要多次重排的元素,position屬性設為absolute或fixed;
不要經常訪問會引起瀏覽器flush隊列的屬性,如果你確實要訪問,利用緩存
~~~
// 不好的寫法
for(循環) {
el.style.left = el.offsetLeft + 5 + "px";
el.style.top = el.offsetTop + 5 + "px";
}
// 比較好的寫法
var left = el.offsetLeft,
top = el.offsetTop,
s = el.style;
for (循環) {
left += 10;
top += 10;
s.left = left + "px";
s.top = top + "px";
}
~~~
#### 讓元素脫離動畫流
具有復雜動畫的元素絕對定位-脫離文檔流,避免強烈的回流。現代瀏覽器可以漸進使用CSS3 transition實現動畫效果,比改變像素值來的高性能。
####在內存中多次操作節點,完成后再添加到文檔中
例如要異步獲取表格數據,渲染到頁面。可以先取得數據后在內存中構建整個表格的html片段,再一次性添加到文檔中去,而不是循環添加每一行。
#### 不要用tables布局
tables中某個元素一旦觸發reflow就會導致table里所有的其它元素reflow。在適合用table的場合,可以設置table-layout為auto或fixed,這樣可以讓table一行一行的渲染,這種做法也是為了限制reflow的影響范圍。
#### 適當定高
例如如果div內容可能有高度差異的動態內容載入。例如右上角的個人用戶信息是頁面渲染完畢之后動態載入的。但是,有可能會出現高度20像素的小圖標,用戶信息,而文字所占據高度為12px * 1.4 = 16.8px, IE6又存在行高被拒的悲劇。因此,如果這部分div不定高,就會出現個人信息載入后,整個頁面下沉幾像素(3.2像素?)頁面重繪的問題。
#### 圖片設定不響應重繪的尺寸
如果你的img標簽不設定尺寸、同時外部容器沒有定死高寬,則圖片在首次載入時候,占據空間會從0到完全出現,左右上下都可能位移,發生大規模的重繪。可以參見新浪微博載入時候頁面高度隨著圖片顯示不斷變高的問題,這些都讓瀏覽器重繪了,一是體驗可能不好,二是燒CPU的。
你可以使用width/height控制,或者在CSS中設置。
<br>
## JavaScript
### 懶執行
懶執行就是將某些邏輯延遲到使用時再計算。該技術可以用于首屏優化,對于某些耗時邏輯并不需要在首屏就使用的,就可以使用懶執行。懶執行需要喚醒,一般可以通過定時器或者事件的調用來喚醒。
### 盡量減少DOM訪問
用JavaScript訪問DOM元素是很慢的,所以,為了讓頁面反應更迅速,應該:
* 緩存已訪問過的元素的索引
* 先“離線”更新節點,再把它們添到DOM樹上
* 避免用JavaScript修復布局問題
<br>
### 批量操作 DOM
在必須要進行頻繁的 DOM 操作時,可以使用 fastdom 這樣的工具,它的思路是將對頁面的讀取和改寫放進隊列,在頁面重繪的時候批量執行,先進行讀取后改寫。因為如果將讀取與改寫交織在一起可能引起多次頁面的重排。而利用 fastdom 就可以避免這樣的情況發生。
雖然有了 fastdom 這樣的工具,但有的時候還是不能從根本上解決問題,比如我最近遇到的一個情況,與頁面簡單的一次交互(輕輕滾動頁面)就執行了幾千次 DOM 操作,這個時候核心要解決的是減少 DOM 操作的次數。這個時候就要從代碼層面考慮,看看是否有不必要的讀取。
<br>
### 利用事件冒泡特性
瀏覽器事件注冊有3個級別定義,DOM 0級事件注冊(利用DOM元素行內事件屬性onclick注冊事件回調),DOM 1級事件注冊(利用DOM元素對象的onclick API 在外部注冊事件回調),DOM 2級事件注冊(利用DOM元素對象的addEventListner/attachEvent API 在外部注冊事件回調)。這里性能優化的建議就是利用DOM2級在目標DOM的父標簽(大部分框架是在body標簽統一注冊事件監聽)注冊回調,收攏事件監聽入口同時節約了DOM節點引用開銷。
<br>
### 把JavaScript和CSS放到外面
很多性能原則都是關于如何管理外部組件的,然而,在這些顧慮出現之前你應該問一個更基礎的問題:應該把JavaScript和CSS放到外部文件中還是直接寫在頁面里?
實際上,用外部文件可以讓頁面更快,因為JavaScript和CSS文件會被緩存在瀏覽器。HTML文檔中的行內JavaScript和CSS在每次請求該HTML文檔的時候都會重新下載。這樣做減少了所需的HTTP請求數,但增加了HTML文檔的大小。另一方面,如果JavaScript和CSS在外部文件中,并且已經被瀏覽器緩存起來了,那么我們就成功地把HTML文檔變小了,而且還沒有增加HTTP請求數。
<br>
### 對高頻觸發的事件進行節流或消抖
debounce 和 throttle 是兩個相似(但不相同)的用于控制函數在某段事件內的執行頻率的技術。你可以在 underscore 或者 lodash 中找到這兩個函數。
使用 debounce 進行消抖
多次連續的調用,最終實際上只會調用一次。想象自己在電梯里面,門將要關上,這個時候另外一個人來了,取消了關門的操作,過了一會兒門又要關上,又來了一個人,再次取消了關門的操作。電梯會一直延遲關門的操作,直到某段時間里沒人再來。
所以 debounce 適合用在比如對用戶輸入內容進行校驗的這種場景下,多次觸發只需要響應最后一次觸發就好了。
使用 throttle 進行節流
將頻繁調用的函數限定在一個給定的調用頻率內。它保證某個函數頻率再高,也只能在給定的事件內調用一次。比如在滾動的時候要檢查當前滾動的位置,來顯示或隱藏回到頂部按鈕,這個時候可以使用 throttle 來將滾動回調函數限定在每 300ms 執行一次。
這兩個函數都接受一個函數作為參數,然后返回一個節流/去抖后的函數:
~~~
// 錯誤的用法,每次事件觸發都得到一個新的函數
$(window).on('scroll', function() {
_.throttle(doSomething, 300);
});
// 正確的用法,將節流后的函數作為回調
$(window).on('scroll', _.throttle(doSomething, 200));
~~~
<br>
### 計算緩存
<br>
### 網絡IO緩存
<br>
### 使用 requestAnimationFrame 來更新頁面
`setTimeout(callback)` 和 `setInterval(callback)` 無法保證 callback 函數的執行時機,很可能在幀結束的時候執行,從而導致丟幀,如下圖:

`requestAnimationFrame(callback)` 可以保證 callback 函數在每幀動畫開始的時候執行。
~~~
// requestAnimationFrame將保證updateScreen函數在每幀的開始運行
requestAnimationFrame(updateScreen);
~~~
注意:jQuery的 `animate` 函數就是用 `setTimeout` 來實現動畫,可以通過[jquery-requestAnimationFrame](https://link.jianshu.com/?t=https://github.com/gnarf/jquery-requestAnimationFrame)這個補丁來用 `requestAnimationFrame` 替代 `setTimeout`

<br>
### 數據結構,算法優化
數據結構和算法的優化是前端接觸比較少的。但是如果碰到計算量比較大的運算,除了運用緩存之外,還要借助一定的數據結構優化和算法優化。
比如現在有50,000條訂單數據。
~~~
const orders = [{name: 'john', price: 20}, {name: 'john', price: 10}, ....]
~~~
我需要頻繁地查找其中某個人某天的訂單信息。 我們可以采取如下的數據結構:
~~~
const mapper = {
'john|2015-09-12': []
}
~~~
這樣我們查找某個人某天的訂單信息速度就會變成O(1),也就是常數時間。你可以理解為索引,因為索引是一種數據結構,那么我們也可以使用其他數據結構和算法適用我們各自獨特的項目。對于算法優化,首先就要求我們能夠識別復雜度,常見的復雜度有O(n) O(logn) O(nlogn) O(n2)。而對于前端,最基本的要識別糟糕的復雜度的代碼,比如n三次方或者n階乘的代碼。雖然我們不需要寫出性能非常好的代碼,但是也盡量不要寫一些復雜度很高的代碼。
<br>
### 多線程計算
通過HTML5的新API webworker,使得開發者可以將計算轉交給worker進程,然后通過進程通信將計算結果回傳給主進程。毫無疑問,這種方法對于需要大量計算有著非常明顯的優勢。
~~~
var dataSortWorker = new Worker("sort-worker.js");
dataSortWorker.postMesssage(dataToSort);
// The main thread is now free to continue working on other things...
dataSortWorker.addEventListener('message', function(evt) {
var sortedData = evt.data;
// Update data on screen...
});
~~~
由于WebWorker 被做了很多限制,使得它不能訪問諸如window,document這樣的對象,因此如果你需要使用的話,就不得不尋找別的方法。
一種使用web worker的思路就是分而治之,將大任務切分為若干個小任務,然后將計算結果匯總,我們通常會借助數組這種數據結構來完成,下面是一個例子:
~~~
// 很多小任務組成的數組
var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
// 使用更新的api requestAnimationFrame而不是setTimeout可以提高性能
requestAnimationFrame(processTaskList);
function processTaskList(taskStartTime) {
var taskFinishTime;
do {
// Assume the next task is pushed onto a stack.
var nextTask = taskList.pop();
// Process nextTask.
processTask(nextTask);
// Go again if there’s enough time to do the next task.
taskFinishTime = window.performance.now();
} while (taskFinishTime - taskStartTime < 3);
if (taskList.length > 0)
requestAnimationFrame(processTaskList);
}
~~~
> 線程安全問題都是由全局變量及靜態變量引起的。 若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,就需要考慮線程同步,就可能產生線程安全問題。
<br>
### 延遲加載
html5中給script標簽引入了async和defer屬性。
帶有async屬性的script標簽,會在瀏覽器解析時立即下載腳本同時不阻塞后續的document渲染和script加載等事件,從而實現腳本的異步加載。
帶有defer屬性的script標簽,和async擁有類似的功能。并且他們有可以附帶一個onload事件`<script src="" defer onload="init()">`。
async和defer的區別在于:async屬性會在腳本下載完成后無序立即執行,defer屬性會在腳本下載完成后按照document結構順序執行。
由于defer和async的兼容性問題,我們通常使用動態創建script標簽的方式來實現異步加載腳本,即
~~~
document.write(' < script src="" async></script>');
~~~
,該方式也可以避免阻塞。
ga統計代碼采用就是動態創建script標簽方案。
該方法不阻塞頁面渲染,不阻塞后續請求,但會阻塞window.onload事件,頁面的表現方式是進度條一直加載或loading菊花一直轉。
所以我們延遲執行ga初始化代碼,將其放到window.onload函數中去執行,可以防止ga腳本阻塞window.onload事件。從而讓用戶感受到更快的加載速度。

### PWA(Progressive Web Apps)方案
<br>
## 圖片
### 懶加載
圖片延遲加載的原理就是先不設置img的src屬性,等合適的時機(比如滾動、滑動、出現在視窗內等)再把圖片真實url放到img的src屬性上。
**固定寬高值的圖片**
固定寬高值的圖片延遲加載比較簡單,因為寬高值都可以設置在css中,只需考慮src的替換問題,推薦使用lazysizes。
~~~
// 引入js文件
<script src="lazysizes.min.js" async=""></script>
// 非響應式 例子
<img src="" data-src="image.jpg" class="lazyload" />
// 響應式 例子,自動計算合適的圖片
<img
data-sizes="auto"
data-src="image2.jpg"
data-srcset="image1.jpg 300w,
image2.jpg 600w,
image3.jpg 900w" class="lazyload" />
// iframe 例子
<iframe frameborder="0"
class="lazyload"
allowfullscreen=""
data-src="//www.youtube.com/embed/ZfV-aYdU4uE">
</iframe>
~~~
lazysizes延遲加載過程中會改變圖片的class:默認lazyload,加載中lazyloading,加載結束:lazyloaded。結合這個特性我們有兩種解決上述問題辦法:
1、設置opacity:0,然后在顯示的時候設置opacity:1。
~~~
// 漸現 lazyload
.lazyload,
.lazyloading{
opacity: 0;
}
.lazyloaded{
opacity: 1;
transition: opacity 500ms; //加上transition就可以實現漸現的效果
}
~~~
2、用一張默認的圖占位,比如1x1的透明圖或者灰圖。
~~~
<img class="lazyload"
src="data:image/gif;base64,R0lGODlhAQABAAA
AACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
data-src="真實url"
alt="<%= article.title %>">
~~~
此外,為了讓效果更佳,尤其是文章詳情頁中的大圖,我們可以加上loading效果。
~~~
.article-detail-bd {
.lazyload {
opacity: 0;
}
.lazyloading {
opacity: 1;
background: #f7f7f7 url(/images/loading.gif) no-repeat center;
}
}
~~~
<br>
**固定寬高比的圖片**
固定寬高比的圖片延遲加載相對來說復雜很多,比如文章詳情頁的圖片,由于設備的寬度值不確定,所以高度值也不確定。
固定寬高比的圖片抖動問題,有下列兩種主流的方式可以解決:
1、第一種方案使用padding-top或者padding-bottom來實現固定寬高比。優點是純CSS方案,缺點是HTML冗余,并且對輸出到第三方不友好。
~~~
<div style="padding-top:75%">
<img data-src="" alt="" class="lazyload">
<div>
~~~
2、第二種方案在頁面初始化階段利用ratio設置實際寬高值,優點是html干凈,對輸出到第三方友好,缺點是依賴js,理論上會至少抖動一次。
~~~
<img data-src="" alt="" class="lazyload" data-ratio="0.75">
~~~
那么,這個 `padding-top: 75%` 和 `data-ratio="0.75"` 的數據從哪兒來呢?在你上傳圖片的時候,需要后臺給你返回原始寬高值,計算得到寬高比,然后保存到data-ratio上。
定義了一個設置圖片高度的函數:
~~~
// 重置圖片高度,僅限文章詳情頁
function resetImgHeight(els, placeholder) {
var ratio = 0,
i, len, width;
for (i = 0, len = els.length; i < len; i++) {
els[i].src = placeholder;
width = els[i].clientWidth; //一定要使用clientWidth
if (els[i].attributes['data-ratio']) {
ratio = els[i].attributes['data-ratio'].value || 0;
ratio = parseFloat(ratio);
}
if (ratio) {
els[i].style.height = (width * ratio) + 'px';
}
}
}
~~~
我們將以上代碼的定義和調用都直接放到了HTML中,就為了一個目的,第一時間計算圖片的高度值,降低用戶感知到頁面抖動的可能性,保證最佳效果。
~~~
// 原生代碼
<img alt=""
data-ratio="0.562500"
data-format="jpeg"
class="lazyload"
data-src="http://img.qdaily.com/uploads/20160807124000WFJNyGam85slTC4H.jpg"
src="">
// 解析之后的代碼
<img alt=""
data-ratio="0.562500"
data-format="jpeg"
class="lazyloaded"
data-src="http://img.qdaily.com/uploads/20160807124000WFJNyGam85slTC4H.jpg"
src="http://img.qdaily.com/uploads/20160807124000WFJNyGam85slTC4H.jpg"
style="height: 323.438px;">
~~~
我們不僅保存了寬高比,還保存了圖片格式,是為了后期可以對gif做進一步的優化。
注意事項
* 避免圖片過早加載,把臨界值調低一點。在實際項目中,并不需要過早就把圖片請求過來,尤其是Mobile項目,過早請求不僅浪費流量,也會因為請求太多,導致頁面加載速度變慢。
* 為了最好的防抖效果,設置圖片高度的JS代碼內嵌到HTML中以便第一時間執行。
* 根據圖片寬度設置高度時,使用clientWidth而不是width。這是因為Safari中,第一時間執行的JS代碼獲取圖片的width失敗,所以使用clientWidth解決這個問題。
<br>
### Low Quality Image Placeholders
原理見:[Progressive Image Loading using Intersection Observer and SQIP](https://calendar.perfplanet.com/2017/progressive-image-loading-using-intersection-observer-and-sqip/)
步驟:
* 頁面初始化時,img元素初始化時,src使用低質量的jpg或svg,顯示出圖片的大概輪廓
* 頁面滾動到當前圖片位置,后臺啟動加載原圖
* 原圖加載完成,替換掉之前的src顯示出原圖
可參考知乎



<br>
### 壓縮圖片體積
首先來看下圖片體積的決定因素。這里可能需要一些圖像學的相關知識。圖片分為位圖和矢量圖。位圖是用比特位來表示像素,然后由像素組成圖片。位圖有一個概念是位深,是指存儲每個像素所用的位數。那么對于位圖計算大小有一個公式就是圖片像素數 * 位深 bits。 注意單位是bits,也可以換算成方便查看的kb或者mb。
> 圖片像素數 = 圖片水平像素數 * 圖片垂直像素數
而矢量圖由數學向量組成,文件容量較小,在進行放大、縮小或旋轉等操作時圖象不會失真,缺點是不易制作色彩變化太多的圖象。那么矢量圖是電腦經過數據計算得到的,因此占據空間小。通常矢量圖和位圖也會相互轉化,比如矢量圖要打印就會點陣化成位圖。
下面講的圖片優化指的是位圖。知道了圖片大小的決定因素,那么減少圖片大小的方式就是減少分辨率或者采用位深較低的圖片格式。
<br>
### 減少分辨率
我們平時開發的時候,設計師會給我們1x2x3x的圖片,這些圖片的像素數是不同的。2x的像素數是1x的 2x2=4倍,而3x的像素數高達3x3=9倍。圖片直接大了9倍。因此前端使用圖片的時候最好不要直接使用3倍圖,然后在不同設備上平鋪,這種做法會需要依賴瀏覽器對其進行重新縮放(這還會占用額外的 CPU 資源)并以較低分辨率顯示,從而降低性能。
<br>
### 減少位深
位深是用來表示一個顏色的字節數。位深是24位,表達的是使用256(2的24/3次方)位表示一個顏色。因此位深越深,圖片越精細。如果可能的話,減少位深可以減少體積。
<br>
### 壓縮
前面說了圖片大小 = 圖片像素數 * 位深, 其實更嚴格的是圖片大小 = 圖片像素數 * 位深 * 圖片質量, 因此圖片質量(q)越低,圖片會越小。 影響圖片壓縮質量的因素有很多,比如圖片的顏色種類數量,相鄰像素顏色相同的個數等等。對應著有很多的圖片壓縮算法,目前比較流行的圖片壓縮是webp格式。因此條件允許的話,盡量使用webp格式。
<br>
### 裁剪
我們希望可以通過 https://test.imgix.net/some_file?w=395&h=96&crop=faces 的方式指定圖片的大小,從而減少傳輸字節的浪費。已經有圖片服務商提供了這樣的功能。比如imgix。
> imgix有一個優勢就是能夠找到圖片中有趣的區域并做裁剪。而不是僅僅裁剪出圖片的中心
>
上面提到的webp最好也可以通過CDN廠商支持,即我們上傳圖片的時候,CDN廠商對應存儲一份webp的。比如我們上傳一個png圖片https://img.alicdn.com/test/TB1XFdma5qAXuNjy1XdXXaYcVXa-29-32.png 。然后我們可以通過https://img.alicdn.com/test/TB1XFdma5qAXuNjy1XdXXaYcVXa-29-32.webp 訪問其對應的webp資源。我們就可以根據瀏覽器的支持情況加載webp或者png圖片了。
<br>
## 參考資料
[漫談前端性能優化](https://juejin.im/post/5a4f09eef265da3e3b7a5399#heading-20)
[2018 前端性能優化清單 - 掘金](https://juejin.im/post/5a966bd16fb9a0635172a50a#heading-5)
[AMP項目實戰分享](https://zhuanlan.zhihu.com/p/34751588)
[雅虎前端優化的35條軍規](http://www.cnblogs.com/xianyulaodi/p/5755079.html#_label0)
[前端性能優化](http://mp.weixin.qq.com/s/qglFD2nHFqFBivb8T23Qtg)
[梳理:提高前端性能方面的處理以及不足 ? 張鑫旭-鑫空間-鑫生活](https://www.zhangxinxu.com/wordpress/2013/04/%E5%89%8D%E7%AB%AF%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E7%BB%8F%E9%AA%8C%E5%88%86%E4%BA%AB/)
[完整攻略!讓你的網頁加載時間降低到 1s 內! - 簡書](https://www.jianshu.com/p/d857c3ff78d6)
[CSS性能優化的8個技巧](https://juejin.im/post/5b6133a351882519d346853)
- 第一部分 HTML
- meta
- meta標簽
- HTML5
- 2.1 語義
- 2.2 通信
- 2.3 離線&存儲
- 2.4 多媒體
- 2.5 3D,圖像&效果
- 2.6 性能&集成
- 2.7 設備訪問
- SEO
- Canvas
- 壓縮圖片
- 制作圓角矩形
- 全局屬性
- 第二部分 CSS
- CSS原理
- 層疊上下文(stacking context)
- 外邊距合并
- 塊狀格式化上下文(BFC)
- 盒模型
- important
- 樣式繼承
- 層疊
- 屬性值處理流程
- 分辨率
- 視口
- CSS API
- grid(未完成)
- flex
- 選擇器
- 3D
- Matrix
- AT規則
- line-height 和 vertical-align
- CSS技術
- 居中
- 響應式布局
- 兼容性
- 移動端適配方案
- CSS應用
- CSS Modules(未完成)
- 分層
- 面向對象CSS(未完成)
- 布局
- 三列布局
- 單列等寬,其他多列自適應均勻
- 多列等高
- 圣杯布局
- 雙飛翼布局
- 瀑布流
- 1px問題
- 適配iPhoneX
- 橫屏適配
- 圖片模糊問題
- stylelint
- 第三部分 JavaScript
- JavaScript原理
- 內存空間
- 作用域
- 執行上下文棧
- 變量對象
- 作用域鏈
- this
- 類型轉換
- 閉包(未完成)
- 原型、面向對象
- class和extend
- 繼承
- new
- DOM
- Event Loop
- 垃圾回收機制
- 內存泄漏
- 數值存儲
- 連等賦值
- 基本類型
- 堆棧溢出
- JavaScriptAPI
- document.referrer
- Promise(未完成)
- Object.create
- 遍歷對象屬性
- 寬度、高度
- performance
- 位運算
- tostring( ) 與 valueOf( )方法
- JavaScript技術
- 錯誤
- 異常處理
- 存儲
- Cookie與Session
- ES6(未完成)
- Babel轉碼
- let和const命令
- 變量的解構賦值
- 字符串的擴展
- 正則的擴展
- 數值的擴展
- 數組的擴展
- 函數的擴展
- 對象的擴展
- Symbol
- Set 和 Map 數據結構
- proxy
- Reflect
- module
- AJAX
- ES5
- 嚴格模式
- JSON
- 數組方法
- 對象方法
- 函數方法
- 服務端推送(未完成)
- JavaScript應用
- 復雜判斷
- 3D 全景圖
- 重載
- 上傳(未完成)
- 上傳方式
- 文件格式
- 渲染大量數據
- 圖片裁剪
- 斐波那契數列
- 編碼
- 數組去重
- 淺拷貝、深拷貝
- instanceof
- 模擬 new
- 防抖
- 節流
- 數組扁平化
- sleep函數
- 模擬bind
- 柯里化
- 零碎知識點
- 第四部分 進階
- 計算機原理
- 數據結構(未完成)
- 算法(未完成)
- 排序算法
- 冒泡排序
- 選擇排序
- 插入排序
- 快速排序
- 搜索算法
- 動態規劃
- 二叉樹
- 瀏覽器
- 瀏覽器結構
- 瀏覽器工作原理
- HTML解析
- CSS解析
- 渲染樹構建
- 布局(Layout)
- 渲染
- 瀏覽器輸入 URL 后發生了什么
- 跨域
- 緩存機制
- reflow(回流)和repaint(重繪)
- 渲染層合并
- 編譯(未完成)
- Babel
- 設計模式(未完成)
- 函數式編程(未完成)
- 正則表達式(未完成)
- 性能
- 性能分析
- 性能指標
- 首屏加載
- 優化
- 瀏覽器層面
- HTTP層面
- 代碼層面
- 構建層面
- 移動端首屏優化
- 服務器層面
- bigpipe
- 構建工具
- Gulp
- webpack
- Webpack概念
- Webpack工具
- Webpack優化
- Webpack原理
- 實現loader
- 實現plugin
- tapable
- Webpack打包后代碼
- rollup.js
- parcel
- 模塊化
- ESM
- 安全
- XSS
- CSRF
- 點擊劫持
- 中間人攻擊
- 密碼存儲
- 測試(未完成)
- 單元測試
- E2E測試
- 框架測試
- 樣式回歸測試
- 異步測試
- 自動化測試
- PWA
- PWA官網
- web app manifest
- service worker
- app install banners
- 調試PWA
- PWA教程
- 框架
- MVVM原理
- Vue
- Vue 餓了么整理
- 樣式
- 技巧
- Vue音樂播放器
- Vue源碼
- Virtual Dom
- computed原理
- 數組綁定原理
- 雙向綁定
- nextTick
- keep-alive
- 導航守衛
- 組件通信
- React
- Diff 算法
- Fiber 原理
- batchUpdate
- React 生命周期
- Redux
- 動畫(未完成)
- 異常監控、收集(未完成)
- 數據采集
- Sentry
- 貝塞爾曲線
- 視頻
- 服務端渲染
- 服務端渲染的利與弊
- Vue SSR
- React SSR
- 客戶端
- 離線包
- 第五部分 網絡
- 五層協議
- TCP
- UDP
- HTTP
- 方法
- 首部
- 狀態碼
- 持久連接
- TLS
- content-type
- Redirect
- CSP
- 請求流程
- HTTP/2 及 HTTP/3
- CDN
- DNS
- HTTPDNS
- 第六部分 服務端
- Linux
- Linux命令
- 權限
- XAMPP
- Node.js
- 安裝
- Node模塊化
- 設置環境變量
- Node的event loop
- 進程
- 全局對象
- 異步IO與事件驅動
- 文件系統
- Node錯誤處理
- koa
- koa-compose
- koa-router
- Nginx
- Nginx配置文件
- 代理服務
- 負載均衡
- 獲取用戶IP
- 解決跨域
- 適配PC與移動環境
- 簡單的訪問限制
- 頁面內容修改
- 圖片處理
- 合并請求
- PM2
- MongoDB
- MySQL
- 常用MySql命令
- 自動化(未完成)
- docker
- 創建CLI
- 持續集成
- 持續交付
- 持續部署
- Jenkins
- 部署與發布
- 遠程登錄服務器
- 增強服務器安全等級
- 搭建 Nodejs 生產環境
- 配置 Nginx 實現反向代理
- 管理域名解析
- 配置 PM2 一鍵部署
- 發布上線
- 部署HTTPS
- Node 應用
- 爬蟲(未完成)
- 例子
- 反爬蟲
- 中間件
- body-parser
- connect-redis
- cookie-parser
- cors
- csurf
- express-session
- helmet
- ioredis
- log4js(未完成)
- uuid
- errorhandler
- nodeclub源碼
- app.js
- config.js
- 消息隊列
- RPC
- 性能優化
- 第七部分 總結
- Web服務器
- 目錄結構
- 依賴
- 功能
- 代碼片段
- 整理
- 知識清單、博客
- 項目、組件、庫
- Node代碼
- 面試必考
- 91算法
- 第八部分 工作代碼總結
- 樣式代碼
- 框架代碼
- 組件代碼
- 功能代碼
- 通用代碼