化實(shí)戰(zhàn):圖片壓縮、緩存與CDN全面提速)
博客加載速度的優(yōu)化是個(gè)老生常談的話題但真正愿意一而再、再而三折騰的人其實(shí)不多。我的博客之前狀態(tài)是首屏圖片加載慢、頁面動不動就幾兆打開要轉(zhuǎn)好幾圈。后來花了兩個(gè)周末做了三輪系統(tǒng)優(yōu)化把整體資源體積砍掉了將近七成加載時(shí)間從最開始的五六秒降到了一秒出頭。這篇就把我自己的整個(gè)優(yōu)化過程、踩過的坑、還有那些真正生效的操作細(xì)節(jié)完整記錄下來。先說清楚一件事博客優(yōu)化的目標(biāo)不是把指標(biāo)刷到某工具打滿分而是讓真實(shí)用戶在普通網(wǎng)絡(luò)環(huán)境下也能快速打開頁面、順暢閱讀。我優(yōu)化的對象是一個(gè)以圖文內(nèi)容為主的個(gè)人博客技術(shù)棧是靜態(tài)站點(diǎn)生成器加自定義前端部署在海外托管平臺。這類博客的特點(diǎn)是頁面數(shù)量多、圖片占比高、緩存策略可以自由配置非常適合做前端性能優(yōu)化尤其是資源體積壓縮和加載路徑精簡。1. 優(yōu)化前先體檢把問題量化成指標(biāo)1.1 像做體檢一樣先跑一輪自動化測試我建議你動手之前先別急著壓縮圖片、刪插件。第一步是用工具跑一遍現(xiàn)狀把問題量化出來。我用的是一套組合檢測方式瀏覽器開發(fā)者工具自帶的網(wǎng)絡(luò)面板用于查看每個(gè)具體請求的耗時(shí)和大小同時(shí)配合在線性能分析工具生成整體報(bào)告重點(diǎn)觀察四個(gè)數(shù)字頁面總資源體積包括 HTML、CSS、JavaScript、圖片和字體請求數(shù)量瀏覽器需要發(fā)起的 HTTP 請求次數(shù)首屏可交互時(shí)間用戶從輸入網(wǎng)址到看到主要內(nèi)容并能操作的時(shí)間點(diǎn)最大內(nèi)容繪制時(shí)間也就是首屏最大元素渲染出來的時(shí)間點(diǎn)我博客當(dāng)時(shí)的原始數(shù)據(jù)總資源體積 3.7MB其中有 2.8MB 是圖片請求數(shù)量 63 個(gè)最大內(nèi)容繪制時(shí)間 4.6 秒。這個(gè)數(shù)據(jù)放在移動端 4G 網(wǎng)絡(luò)下體驗(yàn)很差。還有一個(gè)容易忽略的指標(biāo)是“累計(jì)布局偏移”我做了優(yōu)化之后專門測過它反映的是頁面加載過程中元素有沒有發(fā)生明顯位移。這個(gè)方法很重要因?yàn)椴┛臀恼马撏瑘D片、代碼塊和廣告位如果樣式?jīng)]預(yù)留好空間圖片加載完成時(shí)文字會突然跳一下非常影響閱讀。1.2 從體檢數(shù)據(jù)反推優(yōu)先優(yōu)化順序拿到體檢數(shù)據(jù)后不要一股腦全做。我給自己定了一個(gè)原則80% 的改善來自 20% 的操作。圖片體積占到總資源量的 75%那圖片就是首要優(yōu)化對象腳本請求數(shù)量多就優(yōu)先做合并和延后加載CSS 有冗余就做精簡。我自己定的優(yōu)化順序是圖片壓縮與格式轉(zhuǎn)換、代碼精簡與請求合并、緩存策略配置、加載順序控制。這個(gè)順序的核心邏輯是先控制住體積最大的資源再做網(wǎng)絡(luò)請求層面的優(yōu)化最后通過緩存提升重復(fù)訪問體驗(yàn)。2. 圖片體積壓縮收益最明顯的一步2.1 圖片格式選擇和壓縮工具的實(shí)戰(zhàn)組合博客里圖片格式的選擇直接影響體積。我原來的圖片大多是 JPEG 和 PNG 混著用有的截圖直接存成 PNG一張幾 MB。第一輪優(yōu)化時(shí)我把所有配圖做了批量轉(zhuǎn)換和壓縮。具體操作我用了一款開源的命令行壓縮工具對輸出目錄里的所有圖片做批量處理。壓縮參數(shù)方面JPEG 質(zhì)量我設(shè)在 80 到 85 之間對比過 85 和 60 的肉眼差別在屏幕顯示下沒有太大區(qū)別但體積能差出 40%。PNG 圖片我重點(diǎn)做了索引色轉(zhuǎn)換如果圖片色彩不復(fù)雜可以轉(zhuǎn)成 PNG-8體積直接降一個(gè)量級。WebP 格式我也做了批量轉(zhuǎn)換對比發(fā)現(xiàn)同質(zhì)量下 WebP 比 JPEG 再小 25% 到 35%。如果你不想折騰命令行也可以用一些在線工具但要注意隱私問題博客沒有公開上傳的敏感圖片倒是無所謂如果涉及個(gè)人信息還是建議用本地工具。我自己的習(xí)慣是把壓縮腳本寫進(jìn)發(fā)布流程里每次構(gòu)建自動執(zhí)行不用手動處理。2.2 響應(yīng)式圖片讓移動端用戶別加載桌面大圖只做壓縮還不夠。我試過把一張 2400 像素寬的桌面端圖片壓縮到 90KB但是手機(jī)用戶根本不需要那么大的圖這 90KB 其實(shí)也有一部分是浪費(fèi)的。后來我通過srcset和sizes屬性實(shí)現(xiàn)了響應(yīng)式圖片加載。img srcsetimg/hero-480.webp 480w, img/hero-960.webp 960w, img/hero-1440.webp 1440w sizes(max-width: 600px) 480px, (max-width: 1024px) 960px, 1440px srcimg/hero-960.webp alt博客封面圖瀏覽器會根據(jù)當(dāng)前視口寬度自動選擇加載哪張圖片。這個(gè)功能的成本是一次性做三張尺寸的圖片但對真實(shí)用戶的加載體驗(yàn)提升非常明顯。至少要點(diǎn)是srcset配合sizes否則瀏覽器不知道圖片顯示的寬度可能還是會選擇大圖。如果覺得手寫麻煩現(xiàn)在很多靜態(tài)站點(diǎn)生成器都有圖片處理插件可以自動生成多尺寸并替換標(biāo)簽。我博客用的是自定義方案就自己寫了一個(gè)構(gòu)建腳本在構(gòu)建階段自動做縮略圖和格式轉(zhuǎn)換。2.3 懶加載讓首屏只剩真正需要的內(nèi)容懶加載的原理是頁面加載時(shí)只加載可見區(qū)域的圖片其他圖片等到用戶滾動到附近時(shí)才加載。這個(gè)功能我用的是瀏覽器原生屬性loadinglazy一行代碼搞定。但使用原生懶加載有場景限制需要注意iframe 和圖片都可以用但廣告位之類的關(guān)鍵內(nèi)容不要用會影響曝光統(tǒng)計(jì)首屏內(nèi)的圖片不要設(shè)置懶加載否則會導(dǎo)致最大內(nèi)容繪制時(shí)間不降反升懶加載圖片要預(yù)留尺寸占位否則滾動時(shí)布局偏移會很明顯我還做過一個(gè)對比實(shí)驗(yàn)一篇文章里有 12 張配圖不啟用懶加載時(shí)所有圖片會在頁面加載時(shí)一次性請求啟用之后首屏只加載了 2 張剩下 10 張分成兩批在滾動時(shí)加載。首屏資源體積從 2.8MB 降到了 350KB效果非常直觀。3. 代碼與請求精簡去掉每一份多余的重量3.1 清理無用的 CSS 和 JavaScript博客經(jīng)過多次改版后CSS 和 JavaScript 里往往積攢了很多死代碼。我的做法是用 Chrome 開發(fā)者工具的“Coverage”功能打開頁面后記錄哪些代碼沒有執(zhí)行然后逐個(gè)手動清理。這個(gè)過程比較費(fèi)時(shí)但收益不小。我發(fā)現(xiàn)自己當(dāng)時(shí)引用的一個(gè) UI 框架里大部分組件都沒用而且自定義樣式覆蓋了很多默認(rèn)樣式。清理之后 CSS 文件從 182KB 縮到了 46KBJavaScript 從 120KB 縮到了 40KB。這種清理不能只做一次。我給自己的工作流加了一條每三個(gè)月的定期清理計(jì)劃把上次清理后新加的樣式和腳本再檢查一遍。3.2 JavaScript 延遲加載與按需加載博客的 JavaScript 分為兩部分核心部分和增強(qiáng)部分。核心的部分比如頁面渲染邏輯和基礎(chǔ)交互控制在最小范圍內(nèi)增強(qiáng)的部分比如評論系統(tǒng)的腳本、統(tǒng)計(jì)代碼、分享按鈕這些不參與首屏渲染全部改成延遲加載。我用的是defer屬性和動態(tài)加載兩種方式。defer讓腳本在 HTML 解析完之后再執(zhí)行適合需要執(zhí)行的初始化邏輯動態(tài)加載適合完全按需觸發(fā)的功能比如用戶點(diǎn)擊評論按鈕才加載評論腳本。script src/js/core.js defer/script script // 按需加載評論組件 function loadComments() { const s document.createElement(script); s.src /js/comments.js; document.head.appendChild(s); } /script統(tǒng)計(jì)代碼對性能和隱私都有一定影響。我把第三方統(tǒng)計(jì)腳本從同步加載改成了異步加載并且放在 HTML 底部。這個(gè)改動對頁面加載本身的提速不明顯但對可交互時(shí)間有幫助因?yàn)闉g覽器不用等統(tǒng)計(jì)腳本執(zhí)行完就能響應(yīng)用戶操作。3.3 字體子集化別讓一套字體拖慢整頁博客的字體加載是很多優(yōu)化者容易遺漏的環(huán)節(jié)。我之前為了一篇文章的標(biāo)題效果引用了一套包含上百個(gè)字符的字庫文件單個(gè)字重超過 200KB。瀏覽器為了顯示幾個(gè)字就要下載整個(gè)字庫非常不劃算。字體子集化就是只提取頁面真正用到的字符。我用fontsubset工具把自己的正文和標(biāo)題字庫都做了子集化處理中文博客只保留常用三千字而頁面標(biāo)題用的特殊字體單獨(dú)提取文章標(biāo)題中用到的幾十個(gè)字。優(yōu)化后字體文件從 200KB 降到了 28KB 左右中文博客效果尤其明顯。還有一個(gè)細(xì)節(jié)是字體格式優(yōu)先使用 woff2 格式壓縮率比 woff 好 30% 以上。CSS 里通過多格式聲明保留兼容性。另外字體加載策略我采用了font-display: swap這樣頁面文字先用默認(rèn)字體渲染字體文件加載完成后再切換用戶不會看到文字空白期。4. 緩存策略配置讓再次訪問秒開4.1 一套合理的緩存頭配置博客的頁面內(nèi)容分為兩類一類是會變動的 HTML 頁面一類是不會變動的靜態(tài)資源包括圖片、CSS、JavaScript、字體。這兩類資源應(yīng)該設(shè)置不同的 Cache-Control 策略。我在托管平臺的配置文件里加了這樣的規(guī)則location ~* \.(jpg|jpeg|png|webp|gif|svg)$ { expires 30d; add_header Cache-Control public, max-age2592000, immutable; } location ~* \.(css|js)$ { expires 7d; add_header Cache-Control public, max-age604800; } location ~* \.(woff|woff2)$ { expires 365d; add_header Cache-Control public, max-age31536000, immutable; }靜態(tài)資源文件名里加哈希指紋這是緩存策略里很關(guān)鍵的一環(huán)。因?yàn)槲募V底兞瞬糯韮?nèi)容變了瀏覽器才會重新下載。我的構(gòu)建流程會自動給 CSS、JS 文件加上內(nèi)容哈希所以可以放心使用長期緩存。HTML 頁面本身設(shè)置較短的緩存時(shí)間比如 10 分鐘既不會讓用戶每次打開都重新下載頁面也不會讓內(nèi)容更新遲遲不生效。4.2 CDN讓內(nèi)容離用戶更近我博客的源站在海外國內(nèi)訪問跨境線路延遲高圖片和頁面資源的加載時(shí)間波動很大。后來我接入了 CDN 服務(wù)把靜態(tài)資源分發(fā)到多個(gè)邊緣節(jié)點(diǎn)。這里有個(gè)重要原則CDN 緩存策略和源站的 Cache-Control 是配合工作的。我在 CDN 控制臺配置了緩存規(guī)則把圖片和字體文件的緩存時(shí)間再延長CSS 和 JS 保持與源站一致的 7 天。這樣邊緣節(jié)點(diǎn)會緩存這些資源用戶請求時(shí)直接命中節(jié)點(diǎn)源站壓力降低加載速度提升明顯。使用 CDN 之后我特別留意了緩存刷新操作。發(fā)布新文章時(shí)如果改了 CSS 或 JS 文件必須同時(shí)刷新 CDN 緩存否則線上用戶看到的還是舊資源。我自己的發(fā)布腳本里加了 CDN 緩存刷新命令每次構(gòu)建部署后自動執(zhí)行。4.3 預(yù)連接與預(yù)加載主動幫瀏覽器減少等待瀏覽器解析 HTML 時(shí)是順序加載資源的但我們可以主動告訴它某些域名需要提前建立連接某些資源需要提前獲取。這對應(yīng)兩個(gè) HTML 屬性preconnect和preload。link relpreconnect hrefhttps://cdn.example.com link relpreconnect hrefhttps://fonts.example.com crossorigin link relpreload href/css/main.css asstylepreconnect的作用是提前與目標(biāo)域名建立 TCP 連接和 TLS 握手適合用于加載第三方字體、CDN 資源等場景。preload的作用是告訴瀏覽器某個(gè)資源是當(dāng)前頁面必須的請?zhí)崆跋螺d適合用于首屏核心 CSS 和關(guān)鍵字體。這里注意不要濫用preload只對首屏真正需要的資源使用。如果預(yù)加載了用戶根本不會立刻看到的內(nèi)容反而會把帶寬浪費(fèi)在低優(yōu)先級資源上。5. 服務(wù)端與部署層優(yōu)化再省下最后幾毫秒5.1 啟用 Brotli 壓縮比 Gzip 再小一點(diǎn)HTTP 傳輸層面的壓縮很容易被忽略。我原本使用的是默認(rèn)設(shè)置后來的優(yōu)化中對比了 Gzip 和 Brotli 兩種壓縮算法Brotli 對文本類資源的壓縮率比 Gzip 高 15% 到 20%尤其是在 HTML、CSS、JavaScript 這些內(nèi)容上。我的托管平臺支持在配置文件里啟用 Brotlibrotli on; brotli_comp_level 5; brotli_types text/plain text/css application/javascript application/json image/svgxml application/xmlrss;壓縮級別我選的是 5 而不是最高級別的 11因?yàn)閴嚎s級別越高CPU 消耗越大而壓縮率提升已經(jīng)不明顯。對靜態(tài)文件來說預(yù)壓縮是更好的方案構(gòu)建階段直接生成.gz和.br版本的靜態(tài)文件服務(wù)器直接返回不實(shí)時(shí)壓縮性能最優(yōu)。我的博客用的就是預(yù)壓縮方案。5.2 使用 HTTP/2 多路復(fù)用HTTP/2 的多路復(fù)用讓瀏覽器可以在一個(gè)連接上同時(shí)請求多個(gè)資源避免了 HTTP/1.1 時(shí)代瀏覽器對同一域名的并發(fā)連接數(shù)限制。我是在開啟 HTTPS 之后自動獲得 HTTP/2 支持的只需要確認(rèn)服務(wù)器和 CDN 節(jié)點(diǎn)都啟用了 HTTP/2。HTTP/2 搭配前面的優(yōu)化有個(gè)明顯的好處資源分割更精細(xì)了不用為了讓請求數(shù)量變少而把所有代碼合并成一個(gè)巨大的文件。不過也要平衡過小的文件也有開銷我一般把核心 CSS 保持在 10KB 以內(nèi)其他樣式根據(jù)頁面類型拆分成組件級文件。5.3 靜態(tài)化與部署流程構(gòu)建期提前做“體力活”博客優(yōu)化的另一個(gè)維度是把一些計(jì)算從請求期移到構(gòu)建期。我的博客本來就是靜態(tài)站點(diǎn)生成器產(chǎn)出的沒有數(shù)據(jù)庫查詢、沒有服務(wù)端渲染耗時(shí)。但我在構(gòu)建流程里增加了幾個(gè)自動化步驟所有圖片批量壓縮和格式轉(zhuǎn)換CSS 和 JavaScript 壓縮、合并、加哈希HTML 頁面內(nèi)聯(lián)關(guān)鍵 CSS減少首屏 CSS 請求預(yù)生成 Brotli 和 Gzip 壓縮版本這些操作全部通過構(gòu)建腳本自動執(zhí)行。部署到托管平臺時(shí)線上的文件已經(jīng)是優(yōu)化后的“成品”服務(wù)器只需要把這些文件快速返回給用戶。我使用的部署流程是這樣本地或 CI 環(huán)境執(zhí)行構(gòu)建命令生成_site目錄然后同步到托管平臺最后刷新 CDN 緩存。整個(gè)流程可以做到全自動不會出現(xiàn)忘了壓縮圖片就上線的情況。6. 常見問題與排查技巧實(shí)錄6.1 為什么壓縮后圖片顯示模糊了我用一段時(shí)間后發(fā)現(xiàn)一個(gè)典型問題過度追求壓縮導(dǎo)致圖片在 Retina 屏幕上發(fā)虛。原因是我只提供了一張固定尺寸的圖片而高分辨率屏幕需要兩倍的物理像素來顯示。解決方法是配合第 2.2 節(jié)的響應(yīng)式圖片方案為不同分辨率提供不同尺寸的圖片而不是盲目提高壓縮率。同時(shí)把壓縮質(zhì)量閾值控制在 80 到 85不用為了“極限壓縮”犧牲太多清晰度。這里我把圖片優(yōu)化的參數(shù)整理成一個(gè)速查表方便對照使用圖片類型推薦格式質(zhì)量或參數(shù)建議場景說明文章配圖彩色照片WebP兼容 JPEGWebP 質(zhì)量 80 / JPEG 質(zhì)量 82內(nèi)容比較豐富的場景壓縮率換體積截圖、界面圖WebP 或 PNG-8PNG-8 索引色界面截圖色彩不復(fù)雜PNG-8 最佳Logo、圖標(biāo)SVG保持原始矢量格式SVG 天然體積小不要轉(zhuǎn)位圖長圖、背景圖JPEG 或 WebP質(zhì)量 70 到 75長圖壓縮時(shí)注意手機(jī)端漸變色帶6.2 為什么用了緩存還是每次都重新下載我遇到過這個(gè)問題明明設(shè)置了緩存頭但瀏覽器每次刷新頁面還是重新下載。排查后發(fā)現(xiàn)問題出在緩存命中的判定條件上。瀏覽器重新請求資源時(shí)會先校驗(yàn)緩存是否過期或者發(fā)送條件請求到服務(wù)器服務(wù)器返回 304 表示“未修改”時(shí)復(fù)用本地緩存否則返回 200 并重新發(fā)送資源。這個(gè)機(jī)制如果沒配合好就會造成“每次都下載”的感覺。有一個(gè)要點(diǎn)是不要用Cache-Control: no-cache這個(gè)設(shè)置雖然允許緩存但強(qiáng)制每次使用前向服務(wù)器校驗(yàn)效果上等于每次重新下載。我調(diào)整為max-age2592000, immutable之后圖片資源在有效期內(nèi)直接讀本地緩存發(fā)起的條件請求都省了。另外很多人的瀏覽器插件或開發(fā)者工具打開了“禁用緩存”這會讓所有緩存策略都失效。排查速度問題時(shí)建議先關(guān)閉開發(fā)者工具里的緩存禁用開關(guān)再測真實(shí)訪問效果。6.3 為什么優(yōu)化之后工具得分不升反降在線性能分析工具的打分邏輯有時(shí)候會誤導(dǎo)人。有兩個(gè)常見情況第一工具報(bào)告里的事件來源是模擬的固定網(wǎng)絡(luò)環(huán)境它不一定代表真實(shí)用戶網(wǎng)絡(luò)。我在本地網(wǎng)絡(luò)下測試分?jǐn)?shù)可能很高但換到弱網(wǎng)環(huán)境下分?jǐn)?shù)又掉下來。所以工具分?jǐn)?shù)只能作為參考核心還是看真實(shí)用戶的加載時(shí)間。第二某些優(yōu)化動作會暫時(shí)拉低單一指標(biāo)分?jǐn)?shù)。比如我啟用了懶加載后首屏圖片請求變少了最大內(nèi)容繪制時(shí)間反而可能因?yàn)樽煮w加載等延遲而上升。這時(shí)需要做整體權(quán)衡而不是死磕某個(gè)工具指標(biāo)。我的經(jīng)驗(yàn)是把工具報(bào)告當(dāng)成發(fā)現(xiàn)問題的手段而不是優(yōu)化的最終目標(biāo)。每次優(yōu)化完成后自己到真機(jī)環(huán)境用無痕窗口模擬訪問記錄幾組加載時(shí)間這個(gè)數(shù)據(jù)比工具分?jǐn)?shù)更可信。6.4 常見優(yōu)化誤區(qū)和避坑清單最后整理一批我在優(yōu)化過程中踩過的坑和總結(jié)出來的注意事項(xiàng)這條清單值得你收藏不要為壓縮而壓縮。圖片質(zhì)量 70 以下通常會有可見劣化先做尺寸適配再做格式轉(zhuǎn)換最后才是壓縮質(zhì)量調(diào)整不要一次性引入大量第三方庫。每個(gè)第三方腳本都意味著一次請求可能還附帶自己的懶加載邏輯會和你的壓縮策略互相干擾不要忽略 SVG 圖標(biāo)的優(yōu)化。如果用了圖標(biāo)庫建議按需只留下用到的幾個(gè)圖標(biāo)而不是整套圖庫字體引用不要用import不要同時(shí)引用多個(gè)權(quán)重按需用font-display: swap配合子集化建站初期就使用清晰的靜態(tài)資源命名規(guī)范和目錄結(jié)構(gòu)后面做緩存和 CDN 管理會省很多時(shí)間我還想單獨(dú)說一下代碼高亮和一些“看起來正常但實(shí)際很重”的組件。代碼高亮如果引入完整語言包JS 會很大。我用的是按語言注冊的方式只注冊了博客用到的幾種語言體積降了約 60KB。類似的還有數(shù)學(xué)公式渲染、圖表庫都可以按需加載。6.5 優(yōu)化效果復(fù)盤與實(shí)際體驗(yàn)三輪優(yōu)化做下來我的博客核心指標(biāo)變化是這樣的指標(biāo)優(yōu)化前優(yōu)化后首頁總資源體積3.7MB約 700KB請求數(shù)量63 個(gè)18 個(gè)最大內(nèi)容繪制時(shí)間4.6 秒1.1 秒可交互時(shí)間5.8 秒1.4 秒最重圖片體積約 800KB約 90KB這些數(shù)字不是我跑工具測出來的理想值而是我用自己的手機(jī)連著 4G 網(wǎng)絡(luò)實(shí)測的結(jié)果。打開一篇包含多張配圖的文章幾乎是一劃到圖片就顯示出來了完全感覺不到等待。我后來把這個(gè)優(yōu)化經(jīng)驗(yàn)復(fù)用到另一個(gè)小型內(nèi)容站上沒有花太多額外精力還是那套流程效果同樣顯著。這說明了這套方法的可復(fù)制性不管什么類型的博客優(yōu)化路徑都是一樣的先量化問題再控制體積再優(yōu)化請求最后配置緩存。博客速度優(yōu)化這件事不存在“一次優(yōu)化、永久見效”。隨著內(nèi)容增加、組件變更、第三方服務(wù)調(diào)整性能會不斷變化。我自己已經(jīng)養(yǎng)成了每季度做一次檢查和清理的習(xí)慣也建議你做完這輪優(yōu)化之后把用的工具和命令整理成文檔放進(jìn)項(xiàng)目的 README 里這樣下次優(yōu)化或者換設(shè)備環(huán)境時(shí)不用重新摸索。如果你現(xiàn)在手頭博客還沒做這些優(yōu)化強(qiáng)烈建議先跑一輪檢測工具看看自己的數(shù)據(jù)。很多問題都藏在數(shù)據(jù)背后不測永遠(yuǎn)不知道哪些資源拖慢了整個(gè)頁面。