hexo個人網站優化探索

>> 原文鏈接

Contents


前言


1. hexo 是什麼?

hexo 是一個爲了不依賴於後端進行界面實時數據查詢展示而設計的網站開發工具。比如之前曾使用 Node.js 作爲後臺開發過一個博客網站,自己實現後臺邏輯的話需要考慮數據庫存儲、前後端交互、後端 Server 部署啥的。整個流程比較繁雜,在初期可以作爲前端開發者個人建站學習的一種方式。hexo 簡化了這一流程,它將數據存儲和數據獲取這兩方面都通過編譯構建然後本地化集成到前端靜態資源裏。一個例子就是博客網站通常需要翻頁功能來獲取博客文章,傳統開發方式下,獲取下一頁這個操作由前端腳本發起,並由後端 Server 處理請求並返回,但是使用 hexo 之後這整個過程都是在本地一次完成的,hexo 將所有靜態資源在本地建立了索引。

使用 hexo 通常寫手只需要關注 markdown 格式文章的編寫,其餘的網站編譯、構建和發佈的流程都可以交由框架進行處理,所有的網站內容均會被打包成靜態的 html/css/js 文件。hexo 支持自定義插件,也有一個插件社區,如果寫手同時具備前端能力的話也可以發佈自己的插件到社區裏進行開源共享。

2. 何爲優化?

我們通常所講的性能高低可能側重於對網站運行速度快慢的評估,其包括靜態資源及腳本獲取的速度和網站UI界面是否運行流暢。其實廣義上的優化應包括:網站性能優化、網站可訪性優化、網站SEO優化、網站最佳實踐等。

性能優化指標


1. 整體運行性能

  • FCP (First Contentful Paint):從用戶開始發起網站請求到瀏覽器第一次開始渲染網站數據的所用時長。其中提及的第一次開始渲染的網站數據包含網頁的文字、圖片、HTML DOM 結構等,而不包含位於 iframe 中的網頁數據。該指標通常用於衡量本地和服務器首次建立網絡通訊的速度。

  • TTI (Time To Interactive):從用戶開始導航至網站到頁面變爲完全可交互所花費的時間。網站可交互的衡量標準就是:網站展示了實際可用的內容、界面上可見元素的網頁事件已經被成功綁定(比如點擊、拖動等事件)、用戶和頁面交互的反饋時間低於 50 ms。

  • SI (Speed Index):衡量頁面加載過程中內容可視化顯示的速度。通俗來講就是網站界面元素的繪製和呈現速度,如果使用 lighthouse 測量工具的話它會捕獲瀏覽器中處於加載中的頁面的多個圖片幀,然後計算幀之間的視覺渲染進度。

  • TBT (Total Blocking Time):衡量從頁面首次開始渲染(FCP)之後到頁面實際可交互(TTI)的時間。通常我們訪問一個網站時網站整體呈現後,有一段較短的時間我們不能和界面進行交互,比如鼠標點擊、鍵盤按鍵等,這段時間瀏覽器在進行腳本及樣式的加載和執行。

  • LCP (Largest Contentful Paint):測量視口中最大的內容元素何繪製到屏幕
    所需的時間。通常包含這個元素的下載、解析和渲染整個過程。

  • CLS (Cumulative Layout Shift):一個衡量網站加載時整體佈局抖動情況的數值指標。如果一個網站在加載過程中用戶界面多次抖動和閃爍的話會可能引起用戶的輕度不適,因此應該儘量減少網站的重排和重繪次數。

2. 網站可訪問性

  • 網頁背景色和網站文字前景的對比度不能太低,否則會影響用戶閱讀。
  • 網頁鏈接標籤 <a> 最好包含對鏈接的描述信息,比如:<a href="https://github.com/nojsja">[描述- nojsja 的 github 個人界面]</a>
  • html 元素存在 lang 屬性指定當前語言環境。
  • 正確的 html 語義化標籤能讓鍵盤和讀屏器正常工作,通常一個網頁的結構可以用語義化標籤描述爲:
<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 href="https://google.com">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 標籤的 alt 屬性聲明。它指定了替代文本,用於在圖像無法顯示或者用戶禁用圖像顯示時,代替圖像顯示在瀏覽器中的內容。
  • form 元素內部聲明 label 標籤以讓讀屏器正確工作。
  • iframe 元素聲明 title 屬性來描述其內部內容以便於讀屏器工作。
  • aria 無障礙屬性和標籤的使用,相關參考 >> aria reference
  • input[type=image]、object 標籤添加 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 屬性,當我們按 tab 鍵時焦點會依次切換。並且根據鍵盤序列導航的順序,值爲 0 、非法值、或者沒有 tabindex 值的元素應該放置在 tabindex 值爲正值的元素後面:
1) tabindex=負值 (通常是tabindex=“-1”),表示元素是可聚焦的,但是不能通過鍵盤導航來訪問到該元素,用JS做頁面小組件內部鍵盤導航的時候非常有用。
2) tabindex="0" ,表示元素是可聚焦的,並且可以通過鍵盤導航來聚焦到該元素,它的相對順序是當前處於的DOM結構來決定的。
3) tabindex=正值,表示元素是可聚焦的,並且可以通過鍵盤導航來訪問到該元素;它的相對順序按照tabindex 的數值遞增而滯後獲焦。如果多個元素擁有相同的 tabindex,它們的相對順序按照他們在當前DOM中的先後順序決定。
  • table 元素中正確使用 thscope 讓行表頭和列表頭與其數據域一一對應:
<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文本字幕資源以方便聽障人士使用(需要有字幕資源文件):
<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 列表標籤放在容器組件 ulol 中使用。
  • heading 標籤嚴格按照升序聲明,配合 section 或其它元素(例如p標籤)正確反映界面內容結構:
<h1>Page title</h1>
<section>
  <h2>Section Heading</h2>
  …
    <h3>Sub-section Heading</h3>
</section>
  • 使用 <meta charset="UTF-8"> 指定網站字符集編碼。
  • img 元素引用的圖片資源長寬比應該和 img 當前應用的長寬比相同,不然可能造成圖片扭曲。
  • 添加 <!DOCTYPE html> 以防止瀏覽界面渲染異常。

3. 網站是否應用了最佳實踐策略

> 1) 使用 target="_blank"<a> 鏈接如果沒有聲明 rel="noopener noreferrer" 存在安全風險。

當頁面鏈接至使用 target="_blank" 的另一個頁面時,新頁面將與舊頁面在同一個進程上運行。如果新頁面正在執行開銷極大的 JavaScript,舊頁面性能可能會受影響。並且新的頁面可以通過 window.opener 訪問舊的窗口對象,比如它可以使用 window.opener.location = url 將舊頁面導航至不同的網址。

> 2) 檢查瀏覽器端控制檯是否有告警和錯誤提示,通過指示定位問題並解決。

> 3) http 和 https 協議地址不混用

瀏覽器已經逐漸開始禁止不用協議資源的混用,比如使用 http 協議的 web 服務器加載 https 協議開頭的資源,因此可能出現以下幾種情況:

  • 加載了混合內容,但會出現警告;
  • 不加載混合內容,直接會顯示空白內容;
  • 在加載混合內容之前,會出現類似是否“顯示”,或存在不安全風險而被“阻止”的提示!

應該考慮以下方式進行優化:

  • 將站點加載的部分協議混用外部資源放入自己的服務器進行託管;
  • 對於部署了 https 的網站,在網頁聲明 <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"將 http 請求轉換成 https 請求;
  • 對於部署了 https 的網站,在服務器端添加請求首部:header("Content-Security-Policy: upgrade-insecure-requests")也可以達到一樣的效果;
  • 對於同時支持 http 和 https 訪問的網站,考慮使用相對協議,而不直接明文指定協議:<script src="//path/to/js">,瀏覽器發送請求時會根據當前頁面協議進行動態切換。
  • 類同於使用相對協議,使用相對URL也能達到目的,不過增加了資源之間的耦合度:<script src="./path/to/js"></script>

> 4) 避免使用 AppCache

AppCache已被廢棄 考慮使用service worker的Cache API,

> 5) 避免使用 document.write()

對於網速較慢(2G、3G或較慢的WLAN)的用戶,外部腳本通過document.write()動態注入會使頁面內容的顯示延遲數十秒。

> 6) 避免使用 mutation events

以下mutation events會損害性能,在DOM事件規範中已經棄用:

  • DOMAttrModified
  • DOMAttributeNameChanged
  • DOMCharacterDataModified
  • DOMElementNameChanged
  • DOMNodeInserted
  • DOMNodeInsertedIntoDocument
  • DOMNodeRemoved
  • DOMNodeRemovedFromDocument
  • DOMSubtreeModified

建議使用 MutationObserver 替代

> 7) 避免使用 Web SQL

建議替換爲IndexedDB

> 8) 避免加載過於龐大的 DOM 樹

大型的DOM樹會以多種方式降低頁面性能:

  • 網絡效率和負載性能,如果你的服務器發送一個大的DOM樹,你可能會運送大量不必要的字節。這也可能會減慢頁面加載時間,因爲瀏覽器可能會解析許多沒有顯示在屏幕上的節點。
  • 運行時性能。當用戶和腳本與頁面交互時,瀏覽器必須不斷重新計算節點的位置和樣式。一個大的DOM樹與複雜的樣式規則相結合可能會嚴重減慢渲染速度。
  • 內存性能。如果使用通用查詢選擇器(例如,document.querySelectorAll('li') 您可能會無意中將引用存儲到大量的節點),這可能會壓倒用戶設備的內存功能。

一個最佳的DOM樹:

  • 總共少於1500個節點。
  • 最大深度爲32個節點。
  • 沒有超過60個子節點的父節點。
  • 一般來說,只需要在需要時尋找創建DOM節點的方法,並在不再需要時將其銷燬。

> 9) 允許用戶粘貼密碼

密碼粘貼提高了安全性,因爲它使用戶能夠使用密碼管理器。密碼管理員通常爲用戶生成強密碼,安全地存儲密碼,然後在用戶需要登錄時自動將其粘貼到密碼字段中。
刪除阻止用戶粘貼到密碼字段的代碼。使用事件斷點中的Clipboard paste來打斷點,可以快速找到阻止粘貼密碼的代碼。比如下列這種阻止粘貼密碼的代碼:

var input = document.querySelector('input');
input.addEventListener('paste', (e) => {
  e.preventDefault();
});

4. 網站搜索引擎優化SEO

  • 添加視口聲明 <meta name="viewport"> 並指定 with 和 device-width 來優化移動端顯示。
  • document 添加 title 屬性以讓讀屏器和搜索引擎正確識別網站內容。
  • 添加 meta desctiption 標籤來描述網站內容 <meta name="description" content="Put your description here.">
  • 爲鏈接標籤添加描述文本 <a href="videos.html">basketball videos</a>,以清楚傳達這個超鏈接的指向內容。
  • 使用正確的 href 鏈接地址以讓搜索引擎正確跟蹤實際網址,以下是反例:<a onclick="goto('https://example.com')">
  • 不要使用 meta 標籤來禁用搜索引擎爬蟲爬取你的網頁,以下是反例:<meta name="robots" content="noindex"/>,相對的可以特殊的排除一些爬蟲爬取:<meta name="AdsBot-Google" content="noindex"/>
  • image 圖片元素中使用具有明確意圖和含義的 alt 屬性文字來說明圖片信息,並且避免使用一些非特定指代詞比如: "chart", "image", "diagram"(圖表、圖片)。
  • 不要使用插件來顯示您的內容,即避免使用 embedobjectapplet等標籤來引入資源。
  • 正確放置 robots.txt 到網站根目錄下,它描述了此網站中的哪些內容是不應被搜索引擎的獲取的,哪些是可以被獲取的。通常一些後臺文件(如css、js)和用戶隱私的文件不用被搜索引擎抓取,另外有些文件頻繁被蜘蛛抓取,但是這些文件又是不重要的,那麼可以用robots.txt進行屏蔽。
    一個實例:
User-agent: *
Allow: /
Disallow: /wp-admin/
Disallow: /wp-includes/
Disallow: /wp-content/plugins/
Disallow: /?r=*
  • 向搜索引擎提交網站地圖 sitemap.xml,xml格式的文本就是專門拿來給電腦進行閱讀的語,而 sitemap.xml就是搜尋引擎利用這個規範,讓網站主可以使用它來製作一個包含了網站內所有網頁的目錄檔案,提供給搜尋引擎的爬蟲閱讀。就像是一個地圖一樣,讓搜尋引擎可以知道網站內到底有些什麼網頁。

<?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>

性能測試工具 lighthouse


以上提到的性能監測指標通過 lighthouse 網站性能監測工具可以自動化分析和生成性能測試結果數據,較新版本的 chrome 和 採用 chromium 架構的 edge 瀏覽器都已經自帶了,較低版本的 chrome 瀏覽器可以通過在商店搜索插件安裝。安裝後使用 F12 打開控制檯,切換到 lighthouse 一欄即可直接使用了:

如圖,可以自行決定勾選測試項目和針對測試平臺(桌面/移動端),最後點擊生成報告即可開始運行自動化測試任務,並在測試完成後打開一個分析結果頁面:

結果界面:

至此我們就能對網站的整體性能進行一些評估了,上圖中的PerformanceAccessibilityBest Practices、和 SEO 依次對應上文我們提及的網站整體性能、網站可訪問性、網站最佳實踐、搜索引擎SEO優化等指標。我們可以點擊每一個具體項目來查看測試工具給出的優化建議:

測試結果很大程度上取決於你的 http 靜態資源服務器的資源加載速度,比如在不使用代理的情況下,使用 github pages 服務來託管靜態資源會比使用國內的 gitee pages 託管服務稍慢,而且可能出現部分資源加載失敗的問題。因此國內用戶可以使用 gitee pages 來替代 github pages,不過 gitee 非付費用戶沒有代碼自動構建部署功能,需要使用下文提到的 github action 來進行自動化登錄並觸發構建和部署。

注意: 某些瀏覽器上安裝的插件會影響測試結果,因爲插件可能會發起請求加載其它腳本啥的。這種情況下可以直接使用 npm 包管理器全局安裝 npm install -g lighthouse,然後使用命令行啓動測試流程:lighthouse https://nojsja.gitee.io/blogs --view --preset=desktop --output-path='/home/nojsja/Desktop/lighthouse.html'。最終會根據指定的地址生成一個可直接瀏覽器測試結果網頁文件 lighthouse.html,可以直接打開進行性能排查。

基於 hexo 框架的網站優化


之前寫的一篇文章 《前端性能優化技巧詳解(1)》,詳細的描述了前端性能優化的一些方面,這篇文章不會再羅列每一點,只會對博客中實際應用的優化手段進行說明,大致被劃分成這幾個方面:

  1. 優化資源加載時間
  2. 優化界面運行性能
  3. 網站最佳實踐
  4. 網站 SEO 優化

1. 優化資源加載時間

常見的加載時間優化方式包含以下:

  • 提高網頁資源並行加載能力
  • 延遲加載不必要的外部資源
  • 減少碎片化的外部資源請求,小文件做合併
  • 增加網站帶寬

➣ 使用 defer/async 異步下載 script 腳本資源

HTML在解析時遇到聲明的<script>腳本會立即下載和執行,往往會延遲界面剩餘部分的解析,造成界面白屏的情況。比較古老的優化方式之一就是將腳本放到HTML文檔末尾,這樣子解決了白屏的問題,可是在DOM文檔結構複雜冗長時,也會造成一定的界面腳本下載和執行延遲,script標籤新屬性async和defer可以解決此類問題:

  • defer腳本:聲明 defer 屬性的外部<script>腳本下載時不會阻塞HTML的解析和渲染,並且會在HTML渲染完成並且可實際操作之後開始執行(DOMContentLoaded事件被觸發之前),各個腳本解析執行順序對應聲明時的位置順序,執行完成後會觸發頁面DOMContentLoaded事件。
  • async腳本:聲明 async 屬性的外部<script>腳本下載時不會阻塞HTML的解析和渲染,各個腳本的下載和執行完全獨立,下載完成後即開始執行,所以執行順序不固定,與DOMContentLoaded事件的觸發沒有關聯性。

在我的博客網站中有使用 Bootstrap 外部依賴的樣式和js腳本,但是需要確保他們的聲明順序在靠前的位置,因爲使用異步技術之後,內聯同步執行的其它<script>腳本的執行順序就不能保證了,因此不能使用 defer/async 屬性進行優化。

通常 async/defer 會用於優化一些獨立的子組件依賴腳本的加載,比如用於博客文章中的導航條跳轉的腳本,它的執行順序完全不收到其它部分的制約,因此可以獨立出來使用 async 屬性進行優化。但是需要確保該腳本作用的 導航條 DOM元素聲明位於腳本引入位置之前,以防止出現腳本執行時 DOM 元素還未渲染的狀態,引起腳本錯誤。

➣ 使用 async 函數異步加載外部資源

以下 async 函數作用就是根據傳入的 url 創建 link/script 標籤並添加到 <head> 標籤中以動態加載外部資源,並且在存在回調函數時監聽資源加載情況,加載完畢後再執行回調函數。值得注意的是本方法與直接通過script 聲明依賴資源的不同之處在於不會阻塞界面,腳本的下載和執行都是異步的。

博客中常用於在某個特殊的情況下利用編程方法動態載入外部依賴庫,並在回調函數內進行外部庫的初始化。例如我的博客使用了一個音樂播放器組件,在網頁可視區域滾動到包含這個組件的尚未初始化的 DOM 元素時,就是用 async 來請求服務器的 js 腳本資源,加載完成後在回調函數裏初始化這個音樂播放器。

/**
  * async [異步腳本加載]
  * @author nojsja
  * @param  {String} u [資源地址]
  * @param  {Function} c [回調函數]
  * @param  {String} tag [加載資源類型 link | script]
  * @param  {Boolean} async [加載 script 資源時是否需要聲明 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 事件延遲外部資源加載

通過用戶和界面交互觸發一些事件後,滿足了外部資源加載的條件,再觸發外部資源的加載,也屬於延遲加載的一種優化。

例如我的博客中右側導航條區域有個搜索框可以搜索博客文章,本身這個搜索是通過查找本地預先生成的一個資源索引靜態XML文件來實現的。文章和內容較多這個這個XML文件就會變得龐大,如果在網頁首次加載時就下載它一定會造成網頁帶寬和網絡請求數量的佔用。因此考慮在用戶點擊搜索框將焦點集中到上面時再觸發XML的異步下載,同時爲了不影響使用體驗,可以在加載過程中設置 loading 效果以指示用戶延遲操作。

➣ 使用 preload/prefetch/preconnect 進行預加載優化

  • preload 用來預加載當前頁面所需的資源,如圖像,CSS,JavaScript 和字體文件,它的加載優先級比 prefetch 更高,同時也要注意 preload 並不會阻塞 window 的 onload 事件。博客中有使用它來預加載 css 中引用的字體:<link href="/blogs/fonts/fontawesome-webfont.woff2?v=4.3.0" rel=preload as="font">,針對不同的資源類型需要添加不同的 as 標記信息,如果是跨域加載的話注意添加 crossorigin 屬性聲明。
  • prefetch 是一個低優先級的資源提示,一旦一個頁面加載完畢就會開始下載其他的預加載資源,並且將他們存儲在瀏覽器的緩存中。其中 prefretch 又包含:link、DNS 和 prerendering 三中類型的預加載請求。link prefetch 比如:<link rel="prefetch" href="/path/to/pic.png"> 允許瀏覽器獲取資源並將他們存儲在緩存中;DNS prefetch 允許瀏覽器在用戶瀏覽頁面時在後臺運行 DNS prerender 和 prefetch 非常相似,它們都優化了下一頁資源的未來請求,區別是 prerender 在後臺渲染了整個頁面,因此應該小心使用,可能會造成網絡帶寬的浪費。
  • preconnect 允許瀏覽器在一個 HTTP 請求正式發給服務器前預先執行一些操作,這包括 DNS 解析,TLS 協商,TCP 握手,這消除了往返延遲併爲用戶節省了時間。比如博客中:不算子統計庫的網頁預連接 <link href="http://busuanzi.ibruce.info" rel="preconnect" crossorigin>

➣ 使用 hexo 插件壓縮代碼文件和圖片文件

壓縮靜態資源也是一種節省網絡帶寬,提高請求響應速度的方式。通常採用工程化的方式進行配置,而不用手動對每張圖片進行壓縮。我的博客中使用了 hexo 的一款壓縮插件 Hexo-all-minifier,通過壓縮 HTML、CSS、JS 和圖片來優化博客訪問速度。

安裝:

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 # 壓縮等級
  pngquant: false
  progressive: true # 是否啓用漸進式圖片壓縮

資源的壓縮比較消耗性能和時間,可以考慮在開發環境下不啓用這些插件以加快開發環境啓動。比如單獨指定一個 _config.dev.yml 然後把上述插件全部關閉即可,參考 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"
  }
}

➣ 編寫 hexo-img-lazyload 插件增加圖片懶加載特性

在進行博客優化的時候爲了學習 hexo 自己的插件系統,使用 IntersectionObserver API 來編寫了一個圖片懶加載插件:hexo-img-lazyload,可以通過 npm 命令進行安裝:npm i hexo-img-lazyload

效果預覽:

插件主要原理就是監聽博客構建流程的鉤子事件,拿到構建好的代碼字符串,然後代碼中原生的圖片聲明比如:<img src="path/to/xx.jpg">通過正則全局匹配並替換爲:<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 的代碼注入功能將我們自己編寫的代碼注入到每個構建好的界面中。

hexo 代碼注入:

/* registry scroll listener */
hexo.extend.injector.register('body_end', function() {
  const script = `
    <script>
      ${observerStr}
    </script>`;

  return script;
}, injectionType)

被注入的用於監聽待加載圖片元素是否進入可視區域以進行動態加載的部分代碼,使用了 IntersectionObserver API 而不是 window.onscroll 事件的方式,前者具有更好的性能,由瀏覽器統一監聽所有元素位置信息更改並分發滾動事件結果:


(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由於性能更好已經在我的博客中作爲一種主要的資源懶加載方式,我還使用它來優化博客評論組件 Valine 的加載。一般評論組件都位於博客文章的最下方,因此剛載入文章頁面時完全沒有必要進行評論組件的資源加載,可以考慮使用 IntersectionObserver 監聽評論組件是否進入視口,進入後再使用 async 異步腳本下載並回調執行評論系統初始化。

另一方面每篇文章底部的音樂播放器 Aplayer 也使用了類似的加載策略,可以說優化效果屢試不爽!

➣ 使用 CDN 加載外部依賴腳本

CDN 即內容分發網絡。CDN 服務商將靜態資源緩存到遍佈全國的高性能加速節點上,當用戶訪問相應的業務資源時,CDN系統能夠實時地根據網絡流量和各節點的連接負載狀況、到用戶的距離和響應時間 等綜合信息將用戶的請求重新導向離用戶最近的服務節點上,使內容能夠傳輸的更快,更加穩定。可以提升首次請求的響應能力。

博客中一些公用外部庫比如 BootstrapjQuery 都是使用的外部 CDN 資源地址,一方面可以減小當前主站的網頁帶寬消耗,另一方面 CDN 也能提供一些資源下載加速。

➣ 使用 Aplayer 替代 iframe 加載網易雲音樂

博客之前的版本會在文章界面的底部嵌入一個網易雲音樂自己的播放器,這個播放器其實是一個 iframe 像這樣:

<iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=781246&auto=1&height=66"></iframe>

iframe 加載的時候會加載一堆東西,雖然可以通過 lazy 屬性來進行懶加載,不過iframe 也有很多缺點:

  • iframe會阻塞主頁面的onload事件
  • iframe和主頁面共享HTTP連接池,而瀏覽器對相同域的連接有限制,所以會影響頁面的並行加載
  • iframe不利於網頁佈局
  • iframe對移動端不友好
  • iframe的反覆重新加載可能導致一些瀏覽器的內存泄露
  • iframe中的數據傳輸複雜
  • iframe不利於SEO

新版本將 iframe 播放器換成了 Aplayer 並且把自己喜歡的一個歌曲列表上傳到了另一個 gitee pages 倉庫 進行靜態託管,可以通過以下方式在博客底部加載一個自定義歌曲列表:

var ap = new APlayer({
  container: document.getElementById('aplayer'),
  theme: '#e9e9e9',
  audio: [{
    name: '存在信號',
    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: '遺サレタ場所/斜光',
    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'
  }]
});

預覽圖:

2. 優化界面運行性能

➣ 優化頁面的迴流和重繪情況

1)概念

迴流(重排,reflow)和重繪(repaint)是瀏覽器渲染網頁必不可少的一個過程,迴流主要HTML渲染過程中元素空間位置和大小改變引起的 DOM 樹重新渲染,而重繪是由於節點的樣式屬性發生改變,並不會影響空間佈局。從性能而言迴流的性能消耗大,而且容易產生級聯效應,即一個正常 DOM 樹的流佈局中,一個元素髮生迴流後,該元素位置之後的元素全部都會發生迴流並重新渲染,重繪相對性能消耗更小。

2)怎樣有效判斷界面的迴流和重繪情況?

其實一般基於 chromium 架構瀏覽器都附帶一個網頁開發工具 Devtools,但可以說絕大多數前端開發者都沒有認真瞭解過這個工具的具體用途,只是把它用作簡單的 log調試、網頁請求追蹤和樣式調試這些基礎功能。迴流和重繪也是可以通過它來進行可視化度量的:F12打開 Devtools,找到右上角三個點的摺疊按鈕依次打開 -> More Tools(更多工具) -> Rendering (渲染) - 勾選前兩項 Paint Flashing (高亮重繪區域) 和 Layout Shift Regions (高亮迴流區域),現在重新回到你打開的頁面進行操作,操作過程中發生了迴流的區域會變成藍色,發生了重繪的區域會變成綠色,持續時間不長,注意觀察。

Repaint:

Reflow:

除了用於可視化界面迴流/重繪的情況,Devtools 還有其他一些很實用的功能,比如:Coverage Tools 可以用於分析界面上引入的外部 css/js 腳本內容的使用覆蓋率,就是說我們能通過這個工具衡量引入的外部文件的使用情況,使用頻次較低的外部資源可以考慮內聯或是直接手寫實現,提升引入外部資源的性價比

➣ 使用節流和去抖思想優化滾動事件監聽

在面對一些需要進行調用控制的函數高頻觸發場景時,可能有人會對何時使用節流何時使用去抖產生疑問。這裏通過一個特性進行簡單區分:如果你需要保留短時間內高頻觸發的最後一次結果時,那麼使用去抖函數,如果你需要對函數的調用次數進行限制,以最佳的調用間隔時間保持函數的持續調用而不關心是否是最後一次調用結果時,請使用節流函數。

比如 echarts 圖常常需要在窗口 resize 之後重新使用數據渲染,但是直接監聽 resize 事件可能導致短時間內渲染函數被觸發多次。我們可以使用函數去抖的思想,監聽 resize 事件後在監聽器函數裏獲取參數再使用參數調用事先初始化好了的 throttle 函數,保證 resize 過程結束後能觸發一次實際的 echarts 重渲染即可。

這裏給出節流函數和去抖函數的簡單實現:

  /**
    * fnDebounce [去抖函數]
    * @author nojsja
    * @param  {Function} fn [需要被包裹的原始函數邏輯]
    * @param  {Numberl} timeout [延遲時間]
    * @return {Function} [高階函數]
    */
  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 [節流函數]
    * @author nojsja
    * @param  {Function} fn [需要被包裹的原始函數邏輯]
    * @param  {Numberl} timeout [延遲時間]
    * @return {Function} [高階函數]
    */
  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));
      }
    };
  };

博客中文章右側的內容導航欄會根據滾動條位置自動切換 fixed 佈局和一般流佈局,這麼做是爲了讓導航欄在閱讀文章過程中也能正常呈現,不會被隱藏到頂部:

  /* 限150ms才能觸發一次滾動檢測 */
  (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 已經用於博客中各個界面組件的懶加載功能,它的性能更好、功能也更加全面。但是網頁開發中我們通常會考慮各個 API 的兼容性,這裏可以通過 Can I Use 這個兼容性報告網站進行查看,從下圖可知這個 API 的兼容情況還是可以的,桌面端很多較高版本瀏覽器均已支持:

因此爲了解決個別低版本瀏覽器的兼容性問題,這裏採用了一種比較極端的處理方式。常規情況下我們需要引入外部 [xxx].polyfill.js (xxx爲相應的 API) 來爲低版本瀏覽器添加相應功能,但是對於高版本瀏覽器自身已經支持了這個 API,卻要重複下載 polyfill 庫,造成網頁請求數和帶寬資源的浪費。因此我這裏不採用這種方式,因爲這個 API 大部分瀏覽器已經支持,我們默認不使用 <script> 標籤引入 polyfill.js 而是通過腳本判斷當前瀏覽器是否支持此 API,如果不支持的話再使用同步XHR請求遠程 下載polyfill 文件,下載後使用 eval(...) 的方式執行整個腳本。使用同步方式會阻塞當前 js 執行線程,請謹慎使用,此處是爲了保證 IntersectionObserver 以高優先級方式被注入到網頁中,不然可能引發一些使用了此 API 腳本錯誤。

<!-- 此腳本被放置在靠近頁面首部某個位置 -->
<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 事件監聽

IntersectionObserver 通常用於界面中的一些相交檢測場景:

  • 圖片懶加載 – 當圖片滾動到可見時才進行加載
  • 內容無限滾動 – 用戶滾動到接近滾動容器底部時直接加載更多數據,而無需用戶操作翻頁,給用戶一種網頁可以無限滾動的錯覺
  • 檢測廣告的曝光情況——爲了計算廣告收益,需要知道廣告元素的曝光情況
  • 在用戶看見某個區域時執行任務、播放視頻

以內容無限滾動爲例,古老的相交檢測方案就是使用 scroll 事件監聽滾動容器,在監聽器函數中獲取滾動元素的幾何屬性判斷元素是否已經滾動到底部。我們知道scrollTop等屬性的獲取和設置都會導致頁面迴流,並且如果界面需要綁定多個監聽函數到scroll事件進行類似操作的時候,頁面性能會大打折扣:

  /* 滾動監聽 */
  onScroll = () => {
    const { 
      scrollTop, scrollHeight, clientHeight
    } = document.querySelector('#target');
    
    /* 已經滾動到底部 */
    // scrollTop(向上滾動的高度);clientHeight(容器可視總高度);scrollHeight(容器的總內容長度)
    if (scrollTop + clientHeight === scrollHeight) { /* do something ... */ }
  }

這裏以一個簡單實現的圖片懶加載功能來介紹下它的使用方式,詳細使用可以查看博客:《前端性能優化技巧詳解(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 - 目標元素的幾何邊界信息
        item.intersectionRatio - 相交比 intersectionRect/boundingClientRect
        item.intersectionRect -  描述根和目標元素的相交區域
        item.isIntersecting - true(相交開始),false(相交結束)
        item.rootBounds - 描述根元素
        item.target - 目標元素
        item.time - 時間原點(網頁在窗口加載完成時的時間點)到交叉被觸發的時間的時間戳
      */
      if (item.isIntersecting) {
        loadImage(item.target);
        observer.unobserve(item.target);
      }
    });
  });

  imagesToLoad.forEach(function(image) {
    intersectionObserver.observe(image);
  });
  
})();

3. 網站最佳實踐

➣ 使用 hexo-abbrlink 插件生成文章鏈接

hexo 框架生成的博客地址默認是 :year/:month/:day/:title 這種格式的,也就是 /年/月/日/標題。當博客標題爲中文時,生成的url鏈接中也出現中文,中文路徑對於搜索引擎優化不友好。複製後的鏈接會被編碼,非常不利於閱讀,也不簡潔。

使用 hexo-abbrlink 可以解決這個問題,安裝插件:npm install hexo-abbrlink --save,在 _config.yml 中添加配置:

permalink: :year/:month/:day/:abbrlink.html/
permalink_defaults:
abbrlink:
  alg: crc32  # 算法:crc16(default) and crc32
  rep: hex    # 進制:dec(default) and hex

之後生成的博客文章就會變成這種:https://post.zz173.com/posts/8ddf18fb.html/,即使更新了文章標題,文章的鏈接也不會改變。

➣ 使用 hexo-filter-nofollow 規避安全風險

hexo-filter-nofollow 插件會爲所有 <a> 鏈接添加屬性 rel="noopener external nofollow noreferrer"

網站內部有大量的外鏈會影響網站的權重,不利於SEO。

  • nofollow:是 Google、Yahoo 和微軟公司前幾年一起提出的一個屬性,鏈接加上這個屬性後就不會被計算權值。nofollow 告訴爬蟲無需追蹤目標頁,爲了對抗 blogspam(博客垃圾留言信息),Google推薦使用nofollow,告訴搜索引擎爬蟲無需抓取目標頁,同時告訴搜索引擎無需將的當前頁的Pagerank傳遞到目標頁。但是如果你是通過 sitemap 直接提交該頁面,爬蟲還是會爬取,這裏的nofollow只是當前頁對目標頁的一種態度,並不代表其他頁對目標頁的態度。
  • noreferrernoopener:當 <a> 標籤使用 target="_blank" 屬性鏈接到另一個頁面時,新頁面將與您的頁面在同一個進程上運行。如果新頁面正在執行開銷極大的 JavaScript,舊頁面性能可能會受影響。並且新頁面可以通過 window.opener 拿到舊頁面窗口對象執行任意操作,具有極大的安全隱患。使用 noopener (兼容屬性 noreferrer) 之後,新打開的頁面就不能拿到舊頁面窗口對象了。
  • external:告訴搜素引擎,這是非本站的鏈接,這個作用相當於 target=“_blank”,減少外部鏈接的 SEO 權重影響。

4. 網站SEO優化

➣ 使用 hexo-generator-sitemap 插件自動生成網站地圖

站點地圖是什麼:

  • 站點地圖描述了一個網站的結構。它可以是一個任意形式的文檔,用作網頁設計的設計工具,也可以是列出網站中所有頁面的一個網頁,通常採用分級形式。這有助於訪問者以及搜索引擎的機器人找到網站中的頁面。
  • 一些開發者認爲網站索引是組織網頁的一種更合適的方式,但是網站索引通常是A-Z索引,只提供訪問特定內容的入口,而一個網站地圖爲整個站點提供了一般的自頂向下的視圖。
  • 網站地圖讓所有頁面可被找到來增強搜索引擎優化的效果。

安裝:

$: npm install hexo-generator-sitemap --save

配置文件_config.yml中添加相關字段:

# sitemap
sitemap:
  path: sitemap.xml
# page url
url: https://nojsja.github.io/blogs 

之後運行 hexo generate 之後就可以自動生成網站地圖 sitemap.xml 了,接下來需要到 Google Search Console 記錄自己的站點並提交相應的站點文件。

➣ 向 Google Search Console 提交個人網站地圖

  • 登錄 Google Search Console
  • 添加自己的網站信息


  • 可以通過幾種方法驗證所有權


  • 提交插件生成的 sitemap.xml

Google Search Console 還能看到自己網站的點擊情況、關鍵詞索引情況等,非常方便。

參考


  1. Lighthouse與Google的移動端最佳實踐
  2. Google web.dev

結語


學習前端性能優化的方方面面,一方面是對我們核心基礎知識的考察,另一方面也能爲我們遇到的一些實際問題提供處理思路,是每個前端人進階的的必經之路。

以上就是本篇文章的所有內容,後續有需要還會繼續更新…

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章