Contents
- ## Contents
- ## 前言
-
## 性能優(yōu)化指標(biāo)
- 1. 整體運(yùn)行性能
- 2. 網(wǎng)站可訪問(wèn)性
-
3. 網(wǎng)站是否應(yīng)用了最佳實(shí)踐策略
- > 1) 使用
target="_blank"的<a>鏈接如果沒(méi)有聲明rel="noopener noreferrer"存在安全風(fēng)險(xiǎn)。 - > 2) 檢查瀏覽器端控制臺(tái)是否有告警和錯(cuò)誤提示,通過(guò)指示定位問(wèn)題并解決。
- > 3) http 和 https 協(xié)議地址不混用
- > 4) 避免使用 AppCache
- > 5) 避免使用 document.write()
- > 6) 避免使用 mutation events
- > 7) 避免使用 Web SQL
- > 8) 避免加載過(guò)于龐大的 DOM 樹(shù)
- > 9) 允許用戶(hù)粘貼密碼
- > 1) 使用
- 4. 網(wǎng)站搜索引擎優(yōu)化SEO
- ## 性能測(cè)試工具 lighthouse
-
## 基于 hexo 框架的網(wǎng)站優(yōu)化
-
1. 優(yōu)化資源加載時(shí)間
- ? 使用 defer/async 異步下載 script 腳本資源
- ? 使用 async 函數(shù)異步加載外部資源
- ? 使用瀏覽器 onfocus 事件延遲外部資源加載
- ? 使用 preload/prefetch/preconnect 進(jìn)行預(yù)加載優(yōu)化
- ? 使用 hexo 插件壓縮代碼文件和圖片文件
- ? 編寫(xiě) hexo-img-lazyload 插件增加圖片懶加載特性
- ? 使用 IntersectionObserver API 懶加載其它資源
- ? 使用 CDN 加載外部依賴(lài)腳本
- ? 使用 Aplayer 替代 iframe 加載網(wǎng)易云音樂(lè)
- 2. 優(yōu)化界面運(yùn)行性能
- 3. 網(wǎng)站最佳實(shí)踐
- 4. 網(wǎng)站SEO優(yōu)化
-
1. 優(yōu)化資源加載時(shí)間
- ## 參考
- ## 結(jié)語(yǔ)
前言
1. hexo 是什么?
hexo 是一個(gè)為了不依賴(lài)于后端進(jìn)行界面實(shí)時(shí)數(shù)據(jù)查詢(xún)展示而設(shè)計(jì)的網(wǎng)站開(kāi)發(fā)工具。比如之前曾使用 Node.js 作為后臺(tái)開(kāi)發(fā)過(guò)一個(gè)博客網(wǎng)站,自己實(shí)現(xiàn)后臺(tái)邏輯的話需要考慮數(shù)據(jù)庫(kù)存儲(chǔ)、前后端交互、后端 Server 部署啥的。整個(gè)流程比較繁雜,在初期可以作為前端開(kāi)發(fā)者個(gè)人建站學(xué)習(xí)的一種方式。hexo 簡(jiǎn)化了這一流程,它將數(shù)據(jù)存儲(chǔ)和數(shù)據(jù)獲取這兩方面都通過(guò)編譯構(gòu)建然后本地化集成到前端靜態(tài)資源里。一個(gè)例子就是博客網(wǎng)站通常需要翻頁(yè)功能來(lái)獲取博客文章,傳統(tǒng)開(kāi)發(fā)方式下,獲取下一頁(yè)這個(gè)操作由前端腳本發(fā)起,并由后端 Server 處理請(qǐng)求并返回,但是使用 hexo 之后這整個(gè)過(guò)程都是在本地一次完成的,hexo 將所有靜態(tài)資源在本地建立了索引。
使用 hexo 通常寫(xiě)手只需要關(guān)注 markdown 格式文章的編寫(xiě),其余的網(wǎng)站編譯、構(gòu)建和發(fā)布的流程都可以交由框架進(jìn)行處理,所有的網(wǎng)站內(nèi)容均會(huì)被打包成靜態(tài)的 html/css/js 文件。hexo 支持自定義插件,也有一個(gè)插件社區(qū),如果寫(xiě)手同時(shí)具備前端能力的話也可以發(fā)布自己的插件到社區(qū)里進(jìn)行開(kāi)源共享。
2. 何為優(yōu)化?
我們通常所講的性能高低可能側(cè)重于對(duì)網(wǎng)站運(yùn)行速度快慢的評(píng)估,其包括靜態(tài)資源及腳本獲取的速度和網(wǎng)站UI界面是否運(yùn)行流暢。其實(shí)廣義上的優(yōu)化應(yīng)包括:網(wǎng)站性能優(yōu)化、網(wǎng)站可訪性?xún)?yōu)化、網(wǎng)站SEO優(yōu)化、網(wǎng)站最佳實(shí)踐等。
性能優(yōu)化指標(biāo)
1. 整體運(yùn)行性能
FCP (First Contentful Paint):從用戶(hù)開(kāi)始發(fā)起網(wǎng)站請(qǐng)求到瀏覽器第一次開(kāi)始渲染網(wǎng)站數(shù)據(jù)的所用時(shí)長(zhǎng)。其中提及的第一次開(kāi)始渲染的網(wǎng)站數(shù)據(jù)包含網(wǎng)頁(yè)的文字、圖片、HTML DOM 結(jié)構(gòu)等,而不包含位于 iframe 中的網(wǎng)頁(yè)數(shù)據(jù)。該指標(biāo)通常用于衡量本地和服務(wù)器首次建立網(wǎng)絡(luò)通訊的速度。
TTI (Time To Interactive):從用戶(hù)開(kāi)始導(dǎo)航至網(wǎng)站到頁(yè)面變?yōu)橥耆山换ニㄙM(fèi)的時(shí)間。網(wǎng)站可交互的衡量標(biāo)準(zhǔn)就是:網(wǎng)站展示了實(shí)際可用的內(nèi)容、界面上可見(jiàn)元素的網(wǎng)頁(yè)事件已經(jīng)被成功綁定(比如點(diǎn)擊、拖動(dòng)等事件)、用戶(hù)和頁(yè)面交互的反饋時(shí)間低于 50 ms。
SI (Speed Index):衡量頁(yè)面加載過(guò)程中內(nèi)容可視化顯示的速度。通俗來(lái)講就是網(wǎng)站界面元素的繪制和呈現(xiàn)速度,如果使用 lighthouse 測(cè)量工具的話它會(huì)捕獲瀏覽器中處于加載中的頁(yè)面的多個(gè)圖片幀,然后計(jì)算幀之間的視覺(jué)渲染進(jìn)度。
TBT (Total Blocking Time):衡量從頁(yè)面首次開(kāi)始渲染(FCP)之后到頁(yè)面實(shí)際可交互(TTI)的時(shí)間。通常我們?cè)L問(wèn)一個(gè)網(wǎng)站時(shí)網(wǎng)站整體呈現(xiàn)后,有一段較短的時(shí)間我們不能和界面進(jìn)行交互,比如鼠標(biāo)點(diǎn)擊、鍵盤(pán)按鍵等,這段時(shí)間瀏覽器在進(jìn)行腳本及樣式的加載和執(zhí)行。
LCP (Largest Contentful Paint):測(cè)量視口中最大的內(nèi)容元素何繪制到屏幕所需的時(shí)間。通常包含這個(gè)元素的下載、解析和渲染整個(gè)過(guò)程。
CLS (Cumulative Layout Shift):一個(gè)衡量網(wǎng)站加載時(shí)整體布局抖動(dòng)情況的數(shù)值指標(biāo)。如果一個(gè)網(wǎng)站在加載過(guò)程中用戶(hù)界面多次抖動(dòng)和閃爍的話會(huì)可能引起用戶(hù)的輕度不適,因此應(yīng)該盡量減少網(wǎng)站的重排和重繪次數(shù)。
2. 網(wǎng)站可訪問(wèn)性
- 網(wǎng)頁(yè)背景色和網(wǎng)站文字前景的對(duì)比度不能太低,否則會(huì)影響用戶(hù)閱讀。
- 網(wǎng)頁(yè)鏈接標(biāo)簽
<a>最好包含對(duì)鏈接的描述信息,比如:<a >[描述- nojsja 的 github 個(gè)人界面]</a>。 - html 元素存在
lang屬性指定當(dāng)前語(yǔ)言環(huán)境。 - 正確的 html 語(yǔ)義化標(biāo)簽?zāi)茏屾I盤(pán)和讀屏器正常工作,通常一個(gè)網(wǎng)頁(yè)的結(jié)構(gòu)可以用語(yǔ)義化標(biāo)簽描述為:
<html lang="en">
<head>
<title>Document title</title>
<meta charset="utf-8">
</head>
<body>
<a class="skip-link" href="#maincontent">Skip to main</a>
<h1>Page title</h1>
<nav>
<ul>
<li>
<a >Nav link</a>
</li>
</ul>
</nav>
<header>header</header>
<main id="maincontent">
<section>
<h2>Section heading</h2>
<p>text info</p>
<h3>Sub-section heading</h3>
<p>texgt info</p>
</section>
</main>
<footer>footer</footer>
</body>
</html>
- 界面元素的 id 唯一性。
- img 標(biāo)簽的
alt屬性聲明。它指定了替代文本,用于在圖像無(wú)法顯示或者用戶(hù)禁用圖像顯示時(shí),代替圖像顯示在瀏覽器中的內(nèi)容。 - form 元素內(nèi)部聲明
label標(biāo)簽以讓讀屏器正確工作。 - iframe 元素聲明
title屬性來(lái)描述其內(nèi)部?jī)?nèi)容以便于讀屏器工作。 - aria 無(wú)障礙屬性和標(biāo)簽的使用,相關(guān)參考 >> aria reference
- input[type=image]、object 標(biāo)簽添加
alt屬性聲明:
<input type="image" alt="Sign in" src="./sign-in-button.png">
<object alt="report.pdf type="application/pdf" data="/report.pdf">
Annual report.
</object>
- 需要使用 tab 按鍵聚焦特性的元素可以聲明
tabindex屬性,當(dāng)我們按 tab 鍵時(shí)焦點(diǎn)會(huì)依次切換。并且根據(jù)鍵盤(pán)序列導(dǎo)航的順序,值為 0 、非法值、或者沒(méi)有 tabindex 值的元素應(yīng)該放置在 tabindex 值為正值的元素后面:
1) tabindex=負(fù)值 (通常是tabindex=“-1”),表示元素是可聚焦的,但是不能通過(guò)鍵盤(pán)導(dǎo)航來(lái)訪問(wèn)到該元素,用JS做頁(yè)面小組件內(nèi)部鍵盤(pán)導(dǎo)航的時(shí)候非常有用。
2) tabindex="0" ,表示元素是可聚焦的,并且可以通過(guò)鍵盤(pán)導(dǎo)航來(lái)聚焦到該元素,它的相對(duì)順序是當(dāng)前處于的DOM結(jié)構(gòu)來(lái)決定的。
3) tabindex=正值,表示元素是可聚焦的,并且可以通過(guò)鍵盤(pán)導(dǎo)航來(lái)訪問(wèn)到該元素;它的相對(duì)順序按照tabindex 的數(shù)值遞增而滯后獲焦。如果多個(gè)元素?fù)碛邢嗤?tabindex,它們的相對(duì)順序按照他們?cè)诋?dāng)前DOM中的先后順序決定。
- table 元素中正確使用
th和scope讓行表頭和列表頭與其數(shù)據(jù)域一一對(duì)應(yīng):
<table>
<caption>My marathon training log</caption>
<thead>
<tr>
<th scope="col">Week</th>
<th scope="col">Total miles</th>
<th scope="col">Longest run</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">1</th>
<td>14</td>
<td>5</td>
</tr>
<tr>
<th scope="row">2</th>
<td>16</td>
<td>6</td>
</tr>
</tbody>
</table>
- video 元素指定
track文本字幕資源以方便聽(tīng)障人士使用(需要有字幕資源文件):
<video width="300" height="200">
<source src="marathonFinishLine.mp4" type="video/mp4">
<track src="captions_en.vtt" kind="captions" srclang="en" label="english_captions">
<track src="audio_desc_en.vtt" kind="descriptions" srclang="en" label="english_description">
<track src="captions_es.vtt" kind="captions" srclang="es" label="spanish_captions">
<track src="audio_desc_es.vtt" kind="descriptions" srclang="es" label="spanish_description">
</video>
- li 列表標(biāo)簽放在容器組件
ul或ol中使用。 - heading 標(biāo)簽嚴(yán)格按照升序聲明,配合
section或其它元素(例如p標(biāo)簽)正確反映界面內(nèi)容結(jié)構(gòu):
<h1>Page title</h1>
<section>
<h2>Section Heading</h2>
…
<h3>Sub-section Heading</h3>
</section>
- 使用
<meta charset="UTF-8">指定網(wǎng)站字符集編碼。 - img 元素引用的圖片資源長(zhǎng)寬比應(yīng)該和 img 當(dāng)前應(yīng)用的長(zhǎng)寬比相同,不然可能造成圖片扭曲。
- 添加
<!DOCTYPE html>以防止瀏覽界面渲染異常。
3. 網(wǎng)站是否應(yīng)用了最佳實(shí)踐策略
> 1) 使用 target="_blank" 的 <a> 鏈接如果沒(méi)有聲明 rel="noopener noreferrer" 存在安全風(fēng)險(xiǎn)。
當(dāng)頁(yè)面鏈接至使用 target="_blank" 的另一個(gè)頁(yè)面時(shí),新頁(yè)面將與舊頁(yè)面在同一個(gè)進(jìn)程上運(yùn)行。如果新頁(yè)面正在執(zhí)行開(kāi)銷(xiāo)極大的 JavaScript,舊頁(yè)面性能可能會(huì)受影響。并且新的頁(yè)面可以通過(guò) window.opener 訪問(wèn)舊的窗口對(duì)象,比如它可以使用 window.opener.location = url 將舊頁(yè)面導(dǎo)航至不同的網(wǎng)址。
> 2) 檢查瀏覽器端控制臺(tái)是否有告警和錯(cuò)誤提示,通過(guò)指示定位問(wèn)題并解決。
> 3) http 和 https 協(xié)議地址不混用
瀏覽器已經(jīng)逐漸開(kāi)始禁止不用協(xié)議資源的混用,比如使用 http 協(xié)議的 web 服務(wù)器加載 https 協(xié)議開(kāi)頭的資源,因此可能出現(xiàn)以下幾種情況:
- 加載了混合內(nèi)容,但會(huì)出現(xiàn)警告;
- 不加載混合內(nèi)容,直接會(huì)顯示空白內(nèi)容;
- 在加載混合內(nèi)容之前,會(huì)出現(xiàn)類(lèi)似是否“顯示”,或存在不安全風(fēng)險(xiǎn)而被“阻止”的提示!
應(yīng)該考慮以下方式進(jìn)行優(yōu)化:
- 將站點(diǎn)加載的部分協(xié)議混用外部資源放入自己的服務(wù)器進(jìn)行托管;
- 對(duì)于部署了 https 的網(wǎng)站,在網(wǎng)頁(yè)聲明
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"將 http 請(qǐng)求轉(zhuǎn)換成 https 請(qǐng)求; - 對(duì)于部署了 https 的網(wǎng)站,在服務(wù)器端添加請(qǐng)求首部:
header("Content-Security-Policy: upgrade-insecure-requests")也可以達(dá)到一樣的效果; - 對(duì)于同時(shí)支持 http 和 https 訪問(wèn)的網(wǎng)站,考慮使用相對(duì)協(xié)議,而不直接明文指定協(xié)議:
<script src="http://path/to/js">,瀏覽器發(fā)送請(qǐng)求時(shí)會(huì)根據(jù)當(dāng)前頁(yè)面協(xié)議進(jìn)行動(dòng)態(tài)切換。 - 類(lèi)同于使用相對(duì)協(xié)議,使用相對(duì)URL也能達(dá)到目的,不過(guò)增加了資源之間的耦合度:
<script src="./path/to/js"></script>
> 4) 避免使用 AppCache
AppCache已被廢棄 考慮使用service worker的Cache API,
> 5) 避免使用 document.write()
對(duì)于網(wǎng)速較慢(2G、3G或較慢的WLAN)的用戶(hù),外部腳本通過(guò)document.write()動(dòng)態(tài)注入會(huì)使頁(yè)面內(nèi)容的顯示延遲數(shù)十秒。
> 6) 避免使用 mutation events
以下mutation events會(huì)損害性能,在DOM事件規(guī)范中已經(jīng)棄用:
- DOMAttrModified
- DOMAttributeNameChanged
- DOMCharacterDataModified
- DOMElementNameChanged
- DOMNodeInserted
- DOMNodeInsertedIntoDocument
- DOMNodeRemoved
- DOMNodeRemovedFromDocument
- DOMSubtreeModified
建議使用 MutationObserver 替代
> 7) 避免使用 Web SQL
建議替換為IndexedDB
> 8) 避免加載過(guò)于龐大的 DOM 樹(shù)
大型的DOM樹(shù)會(huì)以多種方式降低頁(yè)面性能:
- 網(wǎng)絡(luò)效率和負(fù)載性能,如果你的服務(wù)器發(fā)送一個(gè)大的DOM樹(shù),你可能會(huì)運(yùn)送大量不必要的字節(jié)。這也可能會(huì)減慢頁(yè)面加載時(shí)間,因?yàn)闉g覽器可能會(huì)解析許多沒(méi)有顯示在屏幕上的節(jié)點(diǎn)。
- 運(yùn)行時(shí)性能。當(dāng)用戶(hù)和腳本與頁(yè)面交互時(shí),瀏覽器必須不斷重新計(jì)算節(jié)點(diǎn)的位置和樣式。一個(gè)大的DOM樹(shù)與復(fù)雜的樣式規(guī)則相結(jié)合可能會(huì)嚴(yán)重減慢渲染速度。
- 內(nèi)存性能。如果使用通用查詢(xún)選擇器(例如,document.querySelectorAll('li') 您可能會(huì)無(wú)意中將引用存儲(chǔ)到大量的節(jié)點(diǎn)),這可能會(huì)壓倒用戶(hù)設(shè)備的內(nèi)存功能。
一個(gè)最佳的DOM樹(shù):
- 總共少于1500個(gè)節(jié)點(diǎn)。
- 最大深度為32個(gè)節(jié)點(diǎn)。
- 沒(méi)有超過(guò)60個(gè)子節(jié)點(diǎn)的父節(jié)點(diǎn)。
- 一般來(lái)說(shuō),只需要在需要時(shí)尋找創(chuàng)建DOM節(jié)點(diǎn)的方法,并在不再需要時(shí)將其銷(xiāo)毀。
> 9) 允許用戶(hù)粘貼密碼
密碼粘貼提高了安全性,因?yàn)樗褂脩?hù)能夠使用密碼管理器。密碼管理員通常為用戶(hù)生成強(qiáng)密碼,安全地存儲(chǔ)密碼,然后在用戶(hù)需要登錄時(shí)自動(dòng)將其粘貼到密碼字段中。
刪除阻止用戶(hù)粘貼到密碼字段的代碼。使用事件斷點(diǎn)中的Clipboard paste來(lái)打斷點(diǎn),可以快速找到阻止粘貼密碼的代碼。比如下列這種阻止粘貼密碼的代碼:
var input = document.querySelector('input');
input.addEventListener('paste', (e) => {
e.preventDefault();
});
4. 網(wǎng)站搜索引擎優(yōu)化SEO
- 添加視口聲明
<meta name="viewport">并指定 with 和 device-width 來(lái)優(yōu)化移動(dòng)端顯示。 - document 添加
title屬性以讓讀屏器和搜索引擎正確識(shí)別網(wǎng)站內(nèi)容。 - 添加
metadesctiption 標(biāo)簽來(lái)描述網(wǎng)站內(nèi)容<meta name="description" content="Put your description here.">。 - 為鏈接標(biāo)簽添加描述文本
<a href="videos.html">basketball videos</a>,以清楚傳達(dá)這個(gè)超鏈接的指向內(nèi)容。 - 使用正確的 href 鏈接地址以讓搜索引擎正確跟蹤實(shí)際網(wǎng)址,以下是反例:
<a onclick="goto('https://example.com')"> - 不要使用 meta 標(biāo)簽來(lái)禁用搜索引擎爬蟲(chóng)爬取你的網(wǎng)頁(yè),以下是反例:
<meta name="robots" content="noindex"/>,相對(duì)的可以特殊的排除一些爬蟲(chóng)爬?。?code><meta name="AdsBot-Google" content="noindex"/>。 - image 圖片元素中使用具有明確意圖和含義的 alt 屬性文字來(lái)說(shuō)明圖片信息,并且避免使用一些非特定指代詞比如:
"chart", "image", "diagram"(圖表、圖片)。 - 不要使用插件來(lái)顯示您的內(nèi)容,即避免使用
embed、object、applet等標(biāo)簽來(lái)引入資源。 - 正確放置
robots.txt到網(wǎng)站根目錄下,它描述了此網(wǎng)站中的哪些內(nèi)容是不應(yīng)被搜索引擎的獲取的,哪些是可以被獲取的。通常一些后臺(tái)文件(如css、js)和用戶(hù)隱私的文件不用被搜索引擎抓取,另外有些文件頻繁被蜘蛛抓取,但是這些文件又是不重要的,那么可以用robots.txt進(jìn)行屏蔽。
一個(gè)實(shí)例:
User-agent: *
Allow: /
Disallow: /wp-admin/
Disallow: /wp-includes/
Disallow: /wp-content/plugins/
Disallow: /?r=*
- 向搜索引擎提交網(wǎng)站地圖 sitemap.xml,xml格式的文本就是專(zhuān)門(mén)拿來(lái)給電腦進(jìn)行閱讀的語(yǔ),而 sitemap.xml就是搜尋引擎利用這個(gè)規(guī)范,讓網(wǎng)站主可以使用它來(lái)制作一個(gè)包含了網(wǎng)站內(nèi)所有網(wǎng)頁(yè)的目錄檔案,提供給搜尋引擎的爬蟲(chóng)閱讀。就像是一個(gè)地圖一樣,讓搜尋引擎可以知道網(wǎng)站內(nèi)到底有些什么網(wǎng)頁(yè)。
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://nojsja.github.io/blogs/2019/10/26/63567fa4.html/</loc>
<lastmod>2021-04-29T10:02:04.853Z</lastmod>
</url>
<url>
<loc>https://nojsja.github.io/blogs/2020/03/26/7507699.html/</loc>
<lastmod>2021-04-29T10:01:30.661Z</lastmod>
</url>
</urlset>
性能測(cè)試工具 lighthouse
以上提到的性能監(jiān)測(cè)指標(biāo)通過(guò) lighthouse 網(wǎng)站性能監(jiān)測(cè)工具可以自動(dòng)化分析和生成性能測(cè)試結(jié)果數(shù)據(jù),較新版本的 chrome 和 采用 chromium 架構(gòu)的 edge 瀏覽器都已經(jīng)自帶了,較低版本的 chrome 瀏覽器可以通過(guò)在商店搜索插件安裝。安裝后使用 F12 打開(kāi)控制臺(tái),切換到 lighthouse 一欄即可直接使用了:
如圖,可以自行決定勾選測(cè)試項(xiàng)目和針對(duì)測(cè)試平臺(tái)(桌面/移動(dòng)端),最后點(diǎn)擊生成報(bào)告即可開(kāi)始運(yùn)行自動(dòng)化測(cè)試任務(wù),并在測(cè)試完成后打開(kāi)一個(gè)分析結(jié)果頁(yè)面:

結(jié)果界面:

至此我們就能對(duì)網(wǎng)站的整體性能進(jìn)行一些評(píng)估了,上圖中的Performance、Accessibility、Best Practices、和 SEO 依次對(duì)應(yīng)上文我們提及的網(wǎng)站整體性能、網(wǎng)站可訪問(wèn)性、網(wǎng)站最佳實(shí)踐、搜索引擎SEO優(yōu)化等指標(biāo)。我們可以點(diǎn)擊每一個(gè)具體項(xiàng)目來(lái)查看測(cè)試工具給出的優(yōu)化建議:

測(cè)試結(jié)果很大程度上取決于你的 http 靜態(tài)資源服務(wù)器的資源加載速度,比如在不使用代理的情況下,使用 github pages 服務(wù)來(lái)托管靜態(tài)資源會(huì)比使用國(guó)內(nèi)的 gitee pages 托管服務(wù)稍慢,而且可能出現(xiàn)部分資源加載失敗的問(wèn)題。因此國(guó)內(nèi)用戶(hù)可以使用 gitee pages 來(lái)替代 github pages,不過(guò) gitee 非付費(fèi)用戶(hù)沒(méi)有代碼自動(dòng)構(gòu)建部署功能,需要使用下文提到的 github action 來(lái)進(jìn)行自動(dòng)化登錄并觸發(fā)構(gòu)建和部署。
注意: 某些瀏覽器上安裝的插件會(huì)影響測(cè)試結(jié)果,因?yàn)椴寮赡軙?huì)發(fā)起請(qǐng)求加載其它腳本啥的。這種情況下可以直接使用 npm 包管理器全局安裝 npm install -g lighthouse,然后使用命令行啟動(dòng)測(cè)試流程:lighthouse https://nojsja.gitee.io/blogs --view --preset=desktop --output-path='/home/nojsja/Desktop/lighthouse.html'。最終會(huì)根據(jù)指定的地址生成一個(gè)可直接瀏覽器測(cè)試結(jié)果網(wǎng)頁(yè)文件 lighthouse.html,可以直接打開(kāi)進(jìn)行性能排查。
基于 hexo 框架的網(wǎng)站優(yōu)化
之前寫(xiě)的一篇文章 《前端性能優(yōu)化技巧詳解(1)》,詳細(xì)的描述了前端性能優(yōu)化的一些方面,這篇文章不會(huì)再羅列每一點(diǎn),只會(huì)對(duì)博客中實(shí)際應(yīng)用的優(yōu)化手段進(jìn)行說(shuō)明,大致被劃分成這幾個(gè)方面:
- 優(yōu)化資源加載時(shí)間
- 優(yōu)化界面運(yùn)行性能
- 網(wǎng)站最佳實(shí)踐
- 網(wǎng)站 SEO 優(yōu)化
1. 優(yōu)化資源加載時(shí)間
常見(jiàn)的加載時(shí)間優(yōu)化方式包含以下:
- 提高網(wǎng)頁(yè)資源并行加載能力
- 延遲加載不必要的外部資源
- 減少碎片化的外部資源請(qǐng)求,小文件做合并
- 增加網(wǎng)站帶寬
? 使用 defer/async 異步下載 script 腳本資源
HTML在解析時(shí)遇到聲明的<script>腳本會(huì)立即下載和執(zhí)行,往往會(huì)延遲界面剩余部分的解析,造成界面白屏的情況。比較古老的優(yōu)化方式之一就是將腳本放到HTML文檔末尾,這樣子解決了白屏的問(wèn)題,可是在DOM文檔結(jié)構(gòu)復(fù)雜冗長(zhǎng)時(shí),也會(huì)造成一定的界面腳本下載和執(zhí)行延遲,script標(biāo)簽新屬性async和defer可以解決此類(lèi)問(wèn)題:
- defer腳本:聲明 defer 屬性的外部
<script>腳本下載時(shí)不會(huì)阻塞HTML的解析和渲染,并且會(huì)在HTML渲染完成并且可實(shí)際操作之后開(kāi)始執(zhí)行(DOMContentLoaded事件被觸發(fā)之前),各個(gè)腳本解析執(zhí)行順序?qū)?yīng)聲明時(shí)的位置順序,執(zhí)行完成后會(huì)觸發(fā)頁(yè)面DOMContentLoaded事件。 - async腳本:聲明 async 屬性的外部
<script>腳本下載時(shí)不會(huì)阻塞HTML的解析和渲染,各個(gè)腳本的下載和執(zhí)行完全獨(dú)立,下載完成后即開(kāi)始執(zhí)行,所以執(zhí)行順序不固定,與DOMContentLoaded事件的觸發(fā)沒(méi)有關(guān)聯(lián)性。
在我的博客網(wǎng)站中有使用 Bootstrap 外部依賴(lài)的樣式和js腳本,但是需要確保他們的聲明順序在靠前的位置,因?yàn)槭褂卯惒郊夹g(shù)之后,內(nèi)聯(lián)同步執(zhí)行的其它<script>腳本的執(zhí)行順序就不能保證了,因此不能使用 defer/async 屬性進(jìn)行優(yōu)化。
通常 async/defer 會(huì)用于優(yōu)化一些獨(dú)立的子組件依賴(lài)腳本的加載,比如用于博客文章中的導(dǎo)航條跳轉(zhuǎn)的腳本,它的執(zhí)行順序完全不收到其它部分的制約,因此可以獨(dú)立出來(lái)使用 async 屬性進(jìn)行優(yōu)化。但是需要確保該腳本作用的 導(dǎo)航條 DOM元素聲明位于腳本引入位置之前,以防止出現(xiàn)腳本執(zhí)行時(shí) DOM 元素還未渲染的狀態(tài),引起腳本錯(cuò)誤。
? 使用 async 函數(shù)異步加載外部資源
以下 async 函數(shù)作用就是根據(jù)傳入的 url 創(chuàng)建 link/script 標(biāo)簽并添加到 <head> 標(biāo)簽中以動(dòng)態(tài)加載外部資源,并且在存在回調(diào)函數(shù)時(shí)監(jiān)聽(tīng)資源加載情況,加載完畢后再執(zhí)行回調(diào)函數(shù)。值得注意的是本方法與直接通過(guò)script 聲明依賴(lài)資源的不同之處在于不會(huì)阻塞界面,腳本的下載和執(zhí)行都是異步的。
博客中常用于在某個(gè)特殊的情況下利用編程方法動(dòng)態(tài)載入外部依賴(lài)庫(kù),并在回調(diào)函數(shù)內(nèi)進(jìn)行外部庫(kù)的初始化。例如我的博客使用了一個(gè)音樂(lè)播放器組件,在網(wǎng)頁(yè)可視區(qū)域滾動(dòng)到包含這個(gè)組件的尚未初始化的 DOM 元素時(shí),就是用 async 來(lái)請(qǐng)求服務(wù)器的 js 腳本資源,加載完成后在回調(diào)函數(shù)里初始化這個(gè)音樂(lè)播放器。
/**
* async [異步腳本加載]
* @author nojsja
* @param {String} u [資源地址]
* @param {Function} c [回調(diào)函數(shù)]
* @param {String} tag [加載資源類(lèi)型 link | script]
* @param {Boolean} async [加載 script 資源時(shí)是否需要聲明 async 屬性]
*/
function async(u, c, tag, async) {
var head = document.head ||
document.getElementsByTagName('head')[0] ||
document.documentElement;
var d = document, t = tag || 'script',
o = d.createElement(t),
s = d.getElementsByTagName(t)[0];
async = ['async', 'defer'].includes(async) ? async : !!async;
switch(t) {
case 'script':
o.src = u;
if (async) o[async] = true;
break;
case 'link':
o.type = "text/css";
o.href = u;
o.rel = "stylesheet";
break;
default:
o.src = u;
break;
}
/* callback */
if (c) {
if (o.readyState) {//IE
o.onreadystatechange = function (e) {
if (o.readyState == "loaded"
|| o.readyState == "complete") {
o.onreadystatechange = null;
c(null, e)();
}
};
} else {//其他瀏覽器
o.onload = function (e) {
c(null, e);
};
}
}
s.parentNode.insertBefore(o, head.firstChild);
}
? 使用瀏覽器 onfocus 事件延遲外部資源加載
通過(guò)用戶(hù)和界面交互觸發(fā)一些事件后,滿(mǎn)足了外部資源加載的條件,再觸發(fā)外部資源的加載,也屬于延遲加載的一種優(yōu)化。
例如我的博客中右側(cè)導(dǎo)航條區(qū)域有個(gè)搜索框可以搜索博客文章,本身這個(gè)搜索是通過(guò)查找本地預(yù)先生成的一個(gè)資源索引靜態(tài)XML文件來(lái)實(shí)現(xiàn)的。文章和內(nèi)容較多這個(gè)這個(gè)XML文件就會(huì)變得龐大,如果在網(wǎng)頁(yè)首次加載時(shí)就下載它一定會(huì)造成網(wǎng)頁(yè)帶寬和網(wǎng)絡(luò)請(qǐng)求數(shù)量的占用。因此考慮在用戶(hù)點(diǎn)擊搜索框?qū)⒔裹c(diǎn)集中到上面時(shí)再觸發(fā)XML的異步下載,同時(shí)為了不影響使用體驗(yàn),可以在加載過(guò)程中設(shè)置 loading 效果以指示用戶(hù)延遲操作。
? 使用 preload/prefetch/preconnect 進(jìn)行預(yù)加載優(yōu)化
- preload 用來(lái)預(yù)加載當(dāng)前頁(yè)面所需的資源,如圖像,CSS,JavaScript 和字體文件,它的加載優(yōu)先級(jí)比 prefetch 更高,同時(shí)也要注意 preload 并不會(huì)阻塞 window 的 onload 事件。博客中有使用它來(lái)預(yù)加載 css 中引用的字體:
<link href="/blogs/fonts/fontawesome-webfont.woff2?v=4.3.0" rel=preload as="font">,針對(duì)不同的資源類(lèi)型需要添加不同的as標(biāo)記信息,如果是跨域加載的話注意添加crossorigin屬性聲明。 - prefetch 是一個(gè)低優(yōu)先級(jí)的資源提示,一旦一個(gè)頁(yè)面加載完畢就會(huì)開(kāi)始下載其他的預(yù)加載資源,并且將他們存儲(chǔ)在瀏覽器的緩存中。其中 prefretch 又包含:
link、DNS 和 prerendering三中類(lèi)型的預(yù)加載請(qǐng)求。link prefetch 比如:<link rel="prefetch" href="/path/to/pic.png">允許瀏覽器獲取資源并將他們存儲(chǔ)在緩存中;DNS prefetch 允許瀏覽器在用戶(hù)瀏覽頁(yè)面時(shí)在后臺(tái)運(yùn)行 DNS prerender 和 prefetch 非常相似,它們都優(yōu)化了下一頁(yè)資源的未來(lái)請(qǐng)求,區(qū)別是 prerender 在后臺(tái)渲染了整個(gè)頁(yè)面,因此應(yīng)該小心使用,可能會(huì)造成網(wǎng)絡(luò)帶寬的浪費(fèi)。 - preconnect 允許瀏覽器在一個(gè) HTTP 請(qǐng)求正式發(fā)給服務(wù)器前預(yù)先執(zhí)行一些操作,這包括 DNS 解析,TLS 協(xié)商,TCP 握手,這消除了往返延遲并為用戶(hù)節(jié)省了時(shí)間。比如博客中:不算子統(tǒng)計(jì)庫(kù)的網(wǎng)頁(yè)預(yù)連接
<link rel="preconnect" crossorigin>。
? 使用 hexo 插件壓縮代碼文件和圖片文件
壓縮靜態(tài)資源也是一種節(jié)省網(wǎng)絡(luò)帶寬,提高請(qǐng)求響應(yīng)速度的方式。通常采用工程化的方式進(jìn)行配置,而不用手動(dòng)對(duì)每張圖片進(jìn)行壓縮。我的博客中使用了 hexo 的一款壓縮插件 Hexo-all-minifier,通過(guò)壓縮 HTML、CSS、JS 和圖片來(lái)優(yōu)化博客訪問(wèn)速度。
安裝:
npm install hexo-all-minifier --save
在 config.yml 配置文件中啟用:
# ---- 代碼和資源壓縮
html_minifier:
enable: true
exclude:
css_minifier:
enable: true
exclude:
- '*.min.css'
js_minifier:
enable: true
mangle: true
compress: true
exclude:
- '*.min.js'
image_minifier:
enable: true
interlaced: false
multipass: false
optimizationLevel: 2 # 壓縮等級(jí)
pngquant: false
progressive: true # 是否啟用漸進(jìn)式圖片壓縮
資源的壓縮比較消耗性能和時(shí)間,可以考慮在開(kāi)發(fā)環(huán)境下不啟用這些插件以加快開(kāi)發(fā)環(huán)境啟動(dòng)。比如單獨(dú)指定一個(gè) _config.dev.yml 然后把上述插件全部關(guān)閉即可,參考 package.json 中的腳本字段聲明:
{
...
"scripts": {
"prestart": "hexo clean --config config.dev.yml; hexo generate --config config.dev.yml",
"prestart:prod": "hexo clean; hexo generate",
"predeploy": "hexo clean; hexo generate",
"start": "hexo generate --config config.dev.yml; hexo server --config config.dev.yml",
"start:prod": "hexo generate --config config.dev.yml; hexo server",
"performance:prod": "lighthouse https://nojsja.gitee.io/blogs --view --preset=desktop --output-path='/home/nojsja/Desktop/lighthouse.html'",
"performance": "lighthouse http://localhost:4000/blogs --view --preset=desktop --output-path='/home/nojsja/Desktop/lighthouse.html'",
"deploy": "hexo deploy"
}
}
? 編寫(xiě) hexo-img-lazyload 插件增加圖片懶加載特性
在進(jìn)行博客優(yōu)化的時(shí)候?yàn)榱藢W(xué)習(xí) hexo 自己的插件系統(tǒng),使用 IntersectionObserver API 來(lái)編寫(xiě)了一個(gè)圖片懶加載插件:hexo-img-lazyload,可以通過(guò) npm 命令進(jìn)行安裝:npm i hexo-img-lazyload。
效果預(yù)覽:

插件主要原理就是監(jiān)聽(tīng)博客構(gòu)建流程的鉤子事件,拿到構(gòu)建好的代碼字符串,然后代碼中原生的圖片聲明比如:<img src="path/to/xx.jpg">通過(guò)正則全局匹配并替換為:<img src="path/to/loading" data-src="path/to/xx.jpg"> 。
function lazyProcessor(content, replacement) {
return content.replace(/<img(.*?)src="(.*?)"(.*?)>/gi, function (str, p1, p2, p3) {
if (/data-loaded/gi.test(str)) {
return str;
}
if (/no-lazy/gi.test(str)) {
return str;
}
return `<img ${p1} src="${emptyStr}" lazyload data-loading="${replacement}" data-src="${p2}" ${p3}>`;
});
}
替換之后還需要使用 hexo 的代碼注入功能將我們自己編寫(xiě)的代碼注入到每個(gè)構(gòu)建好的界面中。
hexo 代碼注入:
/* registry scroll listener */
hexo.extend.injector.register('body_end', function() {
const script = `
<script>
${observerStr}
</script>`;
return script;
}, injectionType)
被注入的用于監(jiān)聽(tīng)待加載圖片元素是否進(jìn)入可視區(qū)域以進(jìn)行動(dòng)態(tài)加載的部分代碼,使用了 IntersectionObserver API 而不是 window.onscroll 事件的方式,前者具有更好的性能,由瀏覽器統(tǒng)一監(jiān)聽(tīng)所有元素位置信息更改并分發(fā)滾動(dòng)事件結(jié)果:
(function() {
/* avoid garbage collection */
window.hexoLoadingImages = window.hexoLoadingImages || {};
function query(selector) {
return document.querySelectorAll(selector);
}
/* registry listener */
if (window.IntersectionObserver) {
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
// in view port
if (entry.isIntersecting) {
observer.unobserve(entry.target);
// proxy image
var img = new Image();
var imgId = "_img_" + Math.random();
window.hexoLoadingImages[imgId] = img;
img.onload = function() {
entry.target.src = entry.target.getAttribute('data-src');
window.hexoLoadingImages[imgId] = null;
};
img.onerror = function() {
window.hexoLoadingImages[imgId] = null;
}
entry.target.src = entry.target.getAttribute('data-loading');
img.src = entry.target.getAttribute('data-src');
}
});
});
query('img[lazyload]').forEach(function (item) {
observer.observe(item);
});
} else {
/* fallback */
query('img[lazyload]').forEach(function (img) {
img.src = img.getAttribute('data-src');
});
}
}).bind(window)();
? 使用 IntersectionObserver API 懶加載其它資源
IntersectionObserver API由于性能更好已經(jīng)在我的博客中作為一種主要的資源懶加載方式,我還使用它來(lái)優(yōu)化博客評(píng)論組件 Valine 的加載。一般評(píng)論組件都位于博客文章的最下方,因此剛載入文章頁(yè)面時(shí)完全沒(méi)有必要進(jìn)行評(píng)論組件的資源加載,可以考慮使用 IntersectionObserver 監(jiān)聽(tīng)評(píng)論組件是否進(jìn)入視口,進(jìn)入后再使用 async 異步腳本下載并回調(diào)執(zhí)行評(píng)論系統(tǒng)初始化。
另一方面每篇文章底部的音樂(lè)播放器 Aplayer 也使用了類(lèi)似的加載策略,可以說(shuō)優(yōu)化效果屢試不爽!
? 使用 CDN 加載外部依賴(lài)腳本
CDN 即內(nèi)容分發(fā)網(wǎng)絡(luò)。CDN 服務(wù)商將靜態(tài)資源緩存到遍布全國(guó)的高性能加速節(jié)點(diǎn)上,當(dāng)用戶(hù)訪問(wèn)相應(yīng)的業(yè)務(wù)資源時(shí),CDN系統(tǒng)能夠?qū)崟r(shí)地根據(jù)網(wǎng)絡(luò)流量和各節(jié)點(diǎn)的連接負(fù)載狀況、到用戶(hù)的距離和響應(yīng)時(shí)間 等綜合信息將用戶(hù)的請(qǐng)求重新導(dǎo)向離用戶(hù)最近的服務(wù)節(jié)點(diǎn)上,使內(nèi)容能夠傳輸?shù)母?,更加穩(wěn)定。可以提升首次請(qǐng)求的響應(yīng)能力。
博客中一些公用外部庫(kù)比如 Bootstrap 和 jQuery 都是使用的外部 CDN 資源地址,一方面可以減小當(dāng)前主站的網(wǎng)頁(yè)帶寬消耗,另一方面 CDN 也能提供一些資源下載加速。
? 使用 Aplayer 替代 iframe 加載網(wǎng)易云音樂(lè)
博客之前的版本會(huì)在文章界面的底部嵌入一個(gè)網(wǎng)易云音樂(lè)自己的播放器,這個(gè)播放器其實(shí)是一個(gè) iframe 像這樣:
<iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="http://music.163.com/outchain/player?type=2&id=781246&auto=1&height=66"></iframe>
iframe 加載的時(shí)候會(huì)加載一堆東西,雖然可以通過(guò) lazy 屬性來(lái)進(jìn)行懶加載,不過(guò)iframe 也有很多缺點(diǎn):
- iframe會(huì)阻塞主頁(yè)面的onload事件
- iframe和主頁(yè)面共享HTTP連接池,而瀏覽器對(duì)相同域的連接有限制,所以會(huì)影響頁(yè)面的并行加載
- iframe不利于網(wǎng)頁(yè)布局
- iframe對(duì)移動(dòng)端不友好
- iframe的反復(fù)重新加載可能導(dǎo)致一些瀏覽器的內(nèi)存泄露
- iframe中的數(shù)據(jù)傳輸復(fù)雜
- iframe不利于SEO
新版本將 iframe 播放器換成了 Aplayer 并且把自己喜歡的一個(gè)歌曲列表上傳到了另一個(gè) gitee pages 倉(cāng)庫(kù) 進(jìn)行靜態(tài)托管,可以通過(guò)以下方式在博客底部加載一個(gè)自定義歌曲列表:
var ap = new APlayer({
container: document.getElementById('aplayer'),
theme: '#e9e9e9',
audio: [{
name: '存在信號(hào)',
artist: 'AcuticNotes',
url: 'https://nojsja.gitee.io/static-resources/audio/life-signal.mp3',
cover: 'https://nojsja.gitee.io/static-resources/audio/life-signal.jpg'
},
{
name: '遺サレタ場(chǎng)所/斜光',
artist: '岡部啓一',
url: 'https://nojsja.gitee.io/static-resources/audio/%E6%96%9C%E5%85%89.mp3',
cover: 'https://nojsja.gitee.io/static-resources/audio/%E6%96%9C%E5%85%89.jpg'
}]
});
預(yù)覽圖:

2. 優(yōu)化界面運(yùn)行性能
? 優(yōu)化頁(yè)面的回流和重繪情況
1)概念
回流(重排,reflow)和重繪(repaint)是瀏覽器渲染網(wǎng)頁(yè)必不可少的一個(gè)過(guò)程,回流主要HTML渲染過(guò)程中元素空間位置和大小改變引起的 DOM 樹(shù)重新渲染,而重繪是由于節(jié)點(diǎn)的樣式屬性發(fā)生改變,并不會(huì)影響空間布局。從性能而言回流的性能消耗大,而且容易產(chǎn)生級(jí)聯(lián)效應(yīng),即一個(gè)正常 DOM 樹(shù)的流布局中,一個(gè)元素發(fā)生回流后,該元素位置之后的元素全部都會(huì)發(fā)生回流并重新渲染,重繪相對(duì)性能消耗更小。
2)怎樣有效判斷界面的回流和重繪情況?
其實(shí)一般基于 chromium 架構(gòu)瀏覽器都附帶一個(gè)網(wǎng)頁(yè)開(kāi)發(fā)工具 Devtools,但可以說(shuō)絕大多數(shù)前端開(kāi)發(fā)者都沒(méi)有認(rèn)真了解過(guò)這個(gè)工具的具體用途,只是把它用作簡(jiǎn)單的 log調(diào)試、網(wǎng)頁(yè)請(qǐng)求追蹤和樣式調(diào)試這些基礎(chǔ)功能?;亓骱椭乩L也是可以通過(guò)它來(lái)進(jìn)行可視化度量的:F12打開(kāi) Devtools,找到右上角三個(gè)點(diǎn)的折疊按鈕依次打開(kāi) -> More Tools(更多工具) -> Rendering (渲染) - 勾選前兩項(xiàng) Paint Flashing (高亮重繪區(qū)域) 和 Layout Shift Regions (高亮回流區(qū)域),現(xiàn)在重新回到你打開(kāi)的頁(yè)面進(jìn)行操作,操作過(guò)程中發(fā)生了回流的區(qū)域會(huì)變成藍(lán)色,發(fā)生了重繪的區(qū)域會(huì)變成綠色,持續(xù)時(shí)間不長(zhǎng),注意觀察。

Repaint:

Reflow:

除了用于可視化界面回流/重繪的情況,Devtools 還有其他一些很實(shí)用的功能,比如:Coverage Tools 可以用于分析界面上引入的外部 css/js 腳本內(nèi)容的使用覆蓋率,就是說(shuō)我們能通過(guò)這個(gè)工具衡量引入的外部文件的使用情況,使用頻次較低的外部資源可以考慮內(nèi)聯(lián)或是直接手寫(xiě)實(shí)現(xiàn),提升引入外部資源的性?xún)r(jià)比。
? 使用節(jié)流和去抖思想優(yōu)化滾動(dòng)事件監(jiān)聽(tīng)
在面對(duì)一些需要進(jìn)行調(diào)用控制的函數(shù)高頻觸發(fā)場(chǎng)景時(shí),可能有人會(huì)對(duì)何時(shí)使用節(jié)流何時(shí)使用去抖產(chǎn)生疑問(wèn)。這里通過(guò)一個(gè)特性進(jìn)行簡(jiǎn)單區(qū)分:如果你需要保留短時(shí)間內(nèi)高頻觸發(fā)的最后一次結(jié)果時(shí),那么使用去抖函數(shù),如果你需要對(duì)函數(shù)的調(diào)用次數(shù)進(jìn)行限制,以最佳的調(diào)用間隔時(shí)間保持函數(shù)的持續(xù)調(diào)用而不關(guān)心是否是最后一次調(diào)用結(jié)果時(shí),請(qǐng)使用節(jié)流函數(shù)。
比如 echarts 圖常常需要在窗口 resize 之后重新使用數(shù)據(jù)渲染,但是直接監(jiān)聽(tīng) resize 事件可能導(dǎo)致短時(shí)間內(nèi)渲染函數(shù)被觸發(fā)多次。我們可以使用函數(shù)去抖的思想,監(jiān)聽(tīng) resize 事件后在監(jiān)聽(tīng)器函數(shù)里獲取參數(shù)再使用參數(shù)調(diào)用事先初始化好了的 throttle 函數(shù),保證 resize 過(guò)程結(jié)束后能觸發(fā)一次實(shí)際的 echarts 重渲染即可。
這里給出節(jié)流函數(shù)和去抖函數(shù)的簡(jiǎn)單實(shí)現(xiàn):
/**
* fnDebounce [去抖函數(shù)]
* @author nojsja
* @param {Function} fn [需要被包裹的原始函數(shù)邏輯]
* @param {Numberl} timeout [延遲時(shí)間]
* @return {Function} [高階函數(shù)]
*/
var fnDebounce = function(fn, timeout) {
var time = null;
return function() {
if (!time) return time = Date.now();
if (Date.now() - time >= timeout) {
time = null;
return fn.apply(this, [].slice.call(arguments));
} else {
time = Date.now();
}
};
};
/**
* fnThrottle [節(jié)流函數(shù)]
* @author nojsja
* @param {Function} fn [需要被包裹的原始函數(shù)邏輯]
* @param {Numberl} timeout [延遲時(shí)間]
* @return {Function} [高階函數(shù)]
*/
var fnThrottle = function(fn, timeout) {
var time = null;
return function() {
if (!time) return time = Date.now();
if ((Date.now() - time) >= timeout) {
time = null;
return fn.apply(this, [].slice.call(arguments));
}
};
};
博客中文章右側(cè)的內(nèi)容導(dǎo)航欄會(huì)根據(jù)滾動(dòng)條位置自動(dòng)切換 fixed 布局和一般流布局,這么做是為了讓導(dǎo)航欄在閱讀文章過(guò)程中也能正常呈現(xiàn),不會(huì)被隱藏到頂部:
/* 限150ms才能觸發(fā)一次滾動(dòng)檢測(cè) */
(window).on('scroll', fnThrottle(function() {
var rectbase = $$tocBar.getBoundingClientRect();
if (rectbase.top <= 0) {
$toc.css('left', left);
(!$toc.hasClass('toc-fixed')) && $toc.addClass('toc-fixed');
$toc.hasClass('toc-normal') && $toc.removeClass('toc-normal');
} else {
$toc.css('left', '');
$toc.hasClass('toc-fixed') && $toc.removeClass('toc-fixed');
(!$toc.hasClass('toc-normal')) && $toc.addClass('toc-normal');
($$toc.scrollTop > 0) && ($$toc.scrollTop = 0);
}
}, 150));
? IntersectionObserver API 的 polyfill 兼容策略
文章中提到 IntersectionObserver API 已經(jīng)用于博客中各個(gè)界面組件的懶加載功能,它的性能更好、功能也更加全面。但是網(wǎng)頁(yè)開(kāi)發(fā)中我們通常會(huì)考慮各個(gè) API 的兼容性,這里可以通過(guò) Can I Use 這個(gè)兼容性報(bào)告網(wǎng)站進(jìn)行查看,從下圖可知這個(gè) API 的兼容情況還是可以的,桌面端很多較高版本瀏覽器均已支持:

因此為了解決個(gè)別低版本瀏覽器的兼容性問(wèn)題,這里采用了一種比較極端的處理方式。常規(guī)情況下我們需要引入外部 [xxx].polyfill.js (xxx為相應(yīng)的 API) 來(lái)為低版本瀏覽器添加相應(yīng)功能,但是對(duì)于高版本瀏覽器自身已經(jīng)支持了這個(gè) API,卻要重復(fù)下載 polyfill 庫(kù),造成網(wǎng)頁(yè)請(qǐng)求數(shù)和帶寬資源的浪費(fèi)。因此我這里不采用這種方式,因?yàn)檫@個(gè) API 大部分瀏覽器已經(jīng)支持,我們默認(rèn)不使用 <script> 標(biāo)簽引入 polyfill.js 而是通過(guò)腳本判斷當(dāng)前瀏覽器是否支持此 API,如果不支持的話再使用同步XHR請(qǐng)求遠(yuǎn)程 下載polyfill 文件,下載后使用 eval(...) 的方式執(zhí)行整個(gè)腳本。使用同步方式會(huì)阻塞當(dāng)前 js 執(zhí)行線程,請(qǐng)謹(jǐn)慎使用,此處是為了保證 IntersectionObserver 以高優(yōu)先級(jí)方式被注入到網(wǎng)頁(yè)中,不然可能引發(fā)一些使用了此 API 腳本錯(cuò)誤。
<!-- 此腳本被放置在靠近頁(yè)面首部某個(gè)位置 -->
<script>
if ('IntersectionObserver' in window &&
'IntersectionObserverEntry' in window &&
'intersectionRatio' in window.IntersectionObserverEntry.prototype) {
if (!('isIntersecting' in window.IntersectionObserverEntry.prototype)) {
Object.defineProperty(window.IntersectionObserverEntry.prototype,
'isIntersecting', {
get: function () {
return this.intersectionRatio > 0;
}
});
}
} else {
/* load polyfill sync */
sendXMLHttpRequest({
url: '/blogs/js/intersection-observer.js',
async: false,
method: 'get',
callback: function(txt) {
eval(txt);
}
});
}
</script>
? 使用 IntersectionObserver 替代原生 onscroll 事件監(jiān)聽(tīng)
IntersectionObserver 通常用于界面中的一些相交檢測(cè)場(chǎng)景:
- 圖片懶加載 – 當(dāng)圖片滾動(dòng)到可見(jiàn)時(shí)才進(jìn)行加載
- 內(nèi)容無(wú)限滾動(dòng) – 用戶(hù)滾動(dòng)到接近滾動(dòng)容器底部時(shí)直接加載更多數(shù)據(jù),而無(wú)需用戶(hù)操作翻頁(yè),給用戶(hù)一種網(wǎng)頁(yè)可以無(wú)限滾動(dòng)的錯(cuò)覺(jué)
- 檢測(cè)廣告的曝光情況——為了計(jì)算廣告收益,需要知道廣告元素的曝光情況
- 在用戶(hù)看見(jiàn)某個(gè)區(qū)域時(shí)執(zhí)行任務(wù)、播放視頻
以?xún)?nèi)容無(wú)限滾動(dòng)為例,古老的相交檢測(cè)方案就是使用 scroll 事件監(jiān)聽(tīng)滾動(dòng)容器,在監(jiān)聽(tīng)器函數(shù)中獲取滾動(dòng)元素的幾何屬性判斷元素是否已經(jīng)滾動(dòng)到底部。我們知道scrollTop等屬性的獲取和設(shè)置都會(huì)導(dǎo)致頁(yè)面回流,并且如果界面需要綁定多個(gè)監(jiān)聽(tīng)函數(shù)到scroll事件進(jìn)行類(lèi)似操作的時(shí)候,頁(yè)面性能會(huì)大打折扣:
/* 滾動(dòng)監(jiān)聽(tīng) */
onScroll = () => {
const {
scrollTop, scrollHeight, clientHeight
} = document.querySelector('#target');
/* 已經(jīng)滾動(dòng)到底部 */
// scrollTop(向上滾動(dòng)的高度);clientHeight(容器可視總高度);scrollHeight(容器的總內(nèi)容長(zhǎng)度)
if (scrollTop + clientHeight === scrollHeight) { /* do something ... */ }
}
這里以一個(gè)簡(jiǎn)單實(shí)現(xiàn)的圖片懶加載功能來(lái)介紹下它的使用方式,詳細(xì)使用可以查看博客:《前端性能優(yōu)化技巧詳解(1)》。
(function lazyload() {
var imagesToLoad = document.querySelectorAll('image[data-src]');
function loadImage(image) {
image.src = image.getAttribute('data-src');
image.addEventListener('load', function() {
image.removeAttribute('data-src');
});
}
var intersectionObserver = new IntersectionObserver(function(items, observer) {
items.forEach(function(item) {
/* 所有屬性:
item.boundingClientRect - 目標(biāo)元素的幾何邊界信息
item.intersectionRatio - 相交比 intersectionRect/boundingClientRect
item.intersectionRect - 描述根和目標(biāo)元素的相交區(qū)域
item.isIntersecting - true(相交開(kāi)始),false(相交結(jié)束)
item.rootBounds - 描述根元素
item.target - 目標(biāo)元素
item.time - 時(shí)間原點(diǎn)(網(wǎng)頁(yè)在窗口加載完成時(shí)的時(shí)間點(diǎn))到交叉被觸發(fā)的時(shí)間的時(shí)間戳
*/
if (item.isIntersecting) {
loadImage(item.target);
observer.unobserve(item.target);
}
});
});
imagesToLoad.forEach(function(image) {
intersectionObserver.observe(image);
});
})();
3. 網(wǎng)站最佳實(shí)踐
? 使用 hexo-abbrlink 插件生成文章鏈接
hexo 框架生成的博客地址默認(rèn)是 :year/:month/:day/:title 這種格式的,也就是 /年/月/日/標(biāo)題。當(dāng)博客標(biāo)題為中文時(shí),生成的url鏈接中也出現(xiàn)中文,中文路徑對(duì)于搜索引擎優(yōu)化不友好。復(fù)制后的鏈接會(huì)被編碼,非常不利于閱讀,也不簡(jiǎn)潔。
使用 hexo-abbrlink 可以解決這個(gè)問(wèn)題,安裝插件:npm install hexo-abbrlink --save,在 _config.yml 中添加配置:
permalink: :year/:month/:day/:abbrlink.html/
permalink_defaults:
abbrlink:
alg: crc32 # 算法:crc16(default) and crc32
rep: hex # 進(jìn)制:dec(default) and hex
之后生成的博客文章就會(huì)變成這種:https://post.zz173.com/posts/8ddf18fb.html/,即使更新了文章標(biāo)題,文章的鏈接也不會(huì)改變。
? 使用 hexo-filter-nofollow 規(guī)避安全風(fēng)險(xiǎn)
hexo-filter-nofollow 插件會(huì)為所有 <a> 鏈接添加屬性 rel="noopener external nofollow noreferrer"。
網(wǎng)站內(nèi)部有大量的外鏈會(huì)影響網(wǎng)站的權(quán)重,不利于SEO。
-
nofollow:是 Google、Yahoo 和微軟公司前幾年一起提出的一個(gè)屬性,鏈接加上這個(gè)屬性后就不會(huì)被計(jì)算權(quán)值。nofollow 告訴爬蟲(chóng)無(wú)需追蹤目標(biāo)頁(yè),為了對(duì)抗 blogspam(博客垃圾留言信息),Google推薦使用nofollow,告訴搜索引擎爬蟲(chóng)無(wú)需抓取目標(biāo)頁(yè),同時(shí)告訴搜索引擎無(wú)需將的當(dāng)前頁(yè)的Pagerank傳遞到目標(biāo)頁(yè)。但是如果你是通過(guò) sitemap 直接提交該頁(yè)面,爬蟲(chóng)還是會(huì)爬取,這里的nofollow只是當(dāng)前頁(yè)對(duì)目標(biāo)頁(yè)的一種態(tài)度,并不代表其他頁(yè)對(duì)目標(biāo)頁(yè)的態(tài)度。 -
noreferrer和noopener:當(dāng)<a>標(biāo)簽使用target="_blank"屬性鏈接到另一個(gè)頁(yè)面時(shí),新頁(yè)面將與您的頁(yè)面在同一個(gè)進(jìn)程上運(yùn)行。如果新頁(yè)面正在執(zhí)行開(kāi)銷(xiāo)極大的 JavaScript,舊頁(yè)面性能可能會(huì)受影響。并且新頁(yè)面可以通過(guò)window.opener拿到舊頁(yè)面窗口對(duì)象執(zhí)行任意操作,具有極大的安全隱患。使用noopener(兼容屬性noreferrer) 之后,新打開(kāi)的頁(yè)面就不能拿到舊頁(yè)面窗口對(duì)象了。 -
external:告訴搜素引擎,這是非本站的鏈接,這個(gè)作用相當(dāng)于target=“_blank”,減少外部鏈接的 SEO 權(quán)重影響。
? 使用 hexo-deployer-git 和 github workflow 進(jìn)行自動(dòng)化部署
靜態(tài)資源打包生成完成后,我需要將其提交到對(duì)應(yīng)的 github pages 或 gitee pages 倉(cāng)庫(kù)中,當(dāng)需要部署多個(gè)倉(cāng)庫(kù)時(shí),手動(dòng)操作效率非常低。因此這里采用 hexo-deployer-git 插件進(jìn)行自動(dòng)化部署,可以向下面一樣聲明需要部署的倉(cāng)庫(kù)信息,如果有多個(gè)倉(cāng)庫(kù)直接聲明多個(gè) deploy 字段即可:
# Deployment
deploy:
type: git
repository: https://github.com/nojsja/blogs
branch: master
ignore_hidden:
public: false
message: update
值得說(shuō)明的是,非付費(fèi)用戶(hù) gitee pages 倉(cāng)庫(kù)不支持提交后自動(dòng)部署。因此我采用的方案是只聲明一個(gè) deploy 部署倉(cāng)庫(kù)指向 github pages 倉(cāng)庫(kù),然后再通過(guò) github 倉(cāng)庫(kù)自帶的 github workflow 服務(wù)配合 gitee-pages-action 這個(gè)腳本實(shí)現(xiàn)的 gitee 自動(dòng)部署。它的原理就是使用 github 自動(dòng)化工作流將 github 倉(cāng)庫(kù)的代碼同步到 gitee 倉(cāng)庫(kù)中去,然后通過(guò)讀取我們配置的 gitee 賬戶(hù)信息自動(dòng)執(zhí)行登錄 gitee 賬戶(hù)并調(diào)用網(wǎng)頁(yè)的手動(dòng)部署接口,實(shí)現(xiàn)整個(gè)自動(dòng)化部署流程。

4. 網(wǎng)站SEO優(yōu)化
? 使用 hexo-generator-sitemap 插件自動(dòng)生成網(wǎng)站地圖
站點(diǎn)地圖是什么:
- 站點(diǎn)地圖描述了一個(gè)網(wǎng)站的結(jié)構(gòu)。它可以是一個(gè)任意形式的文檔,用作網(wǎng)頁(yè)設(shè)計(jì)的設(shè)計(jì)工具,也可以是列出網(wǎng)站中所有頁(yè)面的一個(gè)網(wǎng)頁(yè),通常采用分級(jí)形式。這有助于訪問(wèn)者以及搜索引擎的機(jī)器人找到網(wǎng)站中的頁(yè)面。
- 一些開(kāi)發(fā)者認(rèn)為網(wǎng)站索引是組織網(wǎng)頁(yè)的一種更合適的方式,但是網(wǎng)站索引通常是A-Z索引,只提供訪問(wèn)特定內(nèi)容的入口,而一個(gè)網(wǎng)站地圖為整個(gè)站點(diǎn)提供了一般的自頂向下的視圖。
- 網(wǎng)站地圖讓所有頁(yè)面可被找到來(lái)增強(qiáng)搜索引擎優(yōu)化的效果。
安裝:
$: npm install hexo-generator-sitemap --save
配置文件_config.yml中添加相關(guān)字段:
# sitemap
sitemap:
path: sitemap.xml
# page url
url: https://nojsja.github.io/blogs
之后運(yùn)行 hexo generate 之后就可以自動(dòng)生成網(wǎng)站地圖 sitemap.xml 了,接下來(lái)需要到 Google Search Console 記錄自己的站點(diǎn)并提交相應(yīng)的站點(diǎn)文件。
? 向 Google Search Console 提交個(gè)人網(wǎng)站地圖
- 登錄 Google Search Console
-
添加自己的網(wǎng)站信息
Search Console -
可以通過(guò)幾種方法驗(yàn)證所有權(quán)
Search Console - 提交插件生成的
sitemap.xml
sitemap.xml
Google Search Console 還能看到自己網(wǎng)站的點(diǎn)擊情況、關(guān)鍵詞索引情況等,非常方便。
參考
結(jié)語(yǔ)
學(xué)習(xí)前端性能優(yōu)化的方方面面,一方面是對(duì)我們核心基礎(chǔ)知識(shí)的考察,另一方面也能為我們遇到的一些實(shí)際問(wèn)題提供處理思路,是每個(gè)前端人進(jìn)階的的必經(jīng)之路。
以上就是本篇文章的所有內(nèi)容,后續(xù)有需要還會(huì)繼續(xù)更新…


