Twikoo最新留言區塊效能最佳化
因為部落格的留言板改用Twikoo,其中的API可取得最新留言資料,所以特別在側邊欄增加一個「最新留言」區塊,目的有兩個:
- 先將後端喚醒(Vercel),前端執行背景更新,讓留言內容可以更快載入。
- 讓訪客知道最新留言有哪些(廢話XD)。
注意
本文由 Gemini 3.1 Pro 語言模型產生。
本文件記錄了 Hexo(Light 主題)從新增「最新留言」區塊,到解決效能瓶頸、消除佈局位移(CLS),以及解決 Vercel 冷啟動延遲的完整最佳化過程。
一、建立側邊欄小工具(Widget)
在 themes/light/layout/_widget/recent_comments.ejs 建立檔案。我們採用原生 fetch API 而非 SDK,以確保首頁載入負擔最小化。
1. HTML 結構與骨架載入效果(Skeleton Screen)
預先放置 5 個骨架元件,防止資料載入前的空白,並預留空間減少佈局位移。
<% if (theme.twikoo && theme.twikoo.enable){ %>
<div class="widget tag">
<h3 class="title">最新留言</h3>
<ul class="entry" id="twikoo-recent-comments">
<li class="twikoo-skeleton">
<div class="sk-text"></div><div class="sk-text" style="width: 70%;"></div>
<div class="sk-meta"></div>
</li>
<li class="twikoo-skeleton">
<div class="sk-text"></div><div class="sk-text" style="width: 40%;"></div>
<div class="sk-meta" style="width: 30%;"></div>
</li>
<li class="twikoo-skeleton">
<div class="sk-text"></div><div class="sk-text" style="width: 85%;"></div>
<div class="sk-meta" style="width: 45%;"></div>
</li>
<li class="twikoo-skeleton">
<div class="sk-text"></div><div class="sk-text" style="width: 50%;"></div>
<div class="sk-meta" style="width: 35%;"></div>
</li>
<li class="twikoo-skeleton">
<div class="sk-text"></div><div class="sk-text" style="width: 75%;"></div>
<div class="sk-meta" style="width: 40%;"></div>
</li>
</ul>
</div>
<% } %>
2. SWR(Stale-While-Revalidate)瞬間載入邏輯
利用 localStorage 實現「秒開」體驗:先顯示舊資料,背景更新後無縫替換。
在上述骨架元件最下方的<div>和<% } %>中間插入JS:
<script>
document.addEventListener("DOMContentLoaded", function() {
const container = document.getElementById('twikoo-recent-comments');
if (!container) return;
const cacheKey = 'twikoo-recent-cache';
// === 模組化:建立專屬的「渲染畫面」函數 ===
function renderComments(data) {
if (!data || data.length === 0) {
container.innerHTML = '<li style="color: #888;">暫無留言</li>';
return;
}
container.innerHTML = ''; // 清除骨架元件或舊快取
data.forEach(function(comment) {
// 1. 內容截斷邏輯
let text = comment.commentText;
if (text.length > 25) {
text = text.substring(0, 25) + '⋯';
}
// 2. 日期格式化
let dateStr = comment.relativeTime;
if (!dateStr) {
const d = new Date(comment.created);
dateStr = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
}
// 3. 建立 DOM
const li = document.createElement('li');
li.innerHTML = `
<a href="${comment.url}#${comment.id}" class="twikoo-comment-text" title="${comment.commentText}">${text}</a>
<div class="twikoo-comment-meta">
<span class="twikoo-comment-nick">${comment.nick}</span> / <span class="twikoo-comment-date">${dateStr}</span>
</div>
`;
container.appendChild(li);
});
}
// === 步驟 1:[瞬間] 嘗試讀取本地快取 ===
try {
const cachedData = localStorage.getItem(cacheKey);
if (cachedData) {
renderComments(JSON.parse(cachedData)); // 瞬間蓋掉骨架元件,印出舊留言
}
} catch (e) {
console.error('讀取快取失敗:', e);
}
// === 步驟 2:[背景] 向 Vercel 發送真實請求 ===
fetch('<%= theme.twikoo.envId %>', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: 'GET_RECENT_COMMENTS',
pageSize: 5, // 抓取 5 筆
includeReply: true // 包含回覆
})
})
.then(res => res.json())
.then(res => {
if (res.data) {
// === 步驟 3:[更新] 存入快取並重新渲染最新內容 ===
try {
localStorage.setItem(cacheKey, JSON.stringify(res.data));
} catch (e) {
console.error('寫入快取失敗:', e);
}
renderComments(res.data); // 如果有新留言,這裡會無縫替換掉畫面
}
})
.catch(err => {
// 容錯機制:如果連線失敗,且畫面上還是骨架元件(代表也沒有快取),才顯示錯誤文字
if (container.innerHTML.includes('twikoo-skeleton')) {
console.error('取得最新留言失敗:', err);
container.innerHTML = '<li style="color: red;">載入失敗</li>';
} else {
// 如果畫面上已經有快取舊資料了,就在後台默默報錯,不驚動訪客
console.warn('背景更新留言失敗,繼續顯示快取資料:', err);
}
});
});
</script>
3. 啟用側邊欄小工具
打開主題設定檔 themes/light/_config.yml,在 widgets: 陣列中加入我們剛寫好的小工具:
widgets:
- search
- category
- recent_comments # <--- 新增這一行
- tag
二、視覺樣式最佳化(CSS)
將以下樣式加入主題的 themes/light/source/css/_custom/twikoo.styl 檔案中。包含「緊湊排版」與「骨架載入效果的呼吸燈動畫」。
/* 1. 調整單則留言的外框 (增加底部空間來補償) */
#twikoo-recent-comments li {
margin-bottom: 15px;
line-height: 1.5;
border-bottom: 1px dashed #eee;
/* 原本是 10px,我們把它加厚到 15px,用來填補中間縮小的空隙 */
padding-bottom: 15px;
}
/* 2. 調整留言內容 (拉近與下方作者的距離) */
.twikoo-comment-text {
display: block;
font-size: 1em;
/* 把原本的 4px (甚至可能是被 Hexo 主題預設撐開的空間) 強制縮小到 2px */
margin-bottom: 2px !important;
line-height: 1.4; /* 稍微收緊多行文字本身的行距,看起來更緊湊 */
word-break: break-all;
}
/* (原本的 .twikoo-comment-meta 保持不變即可) */
.twikoo-comment-meta {
font-size: 0.9em;
color: #888;
}
/* =========================================
Twikoo 最新留言 - 骨架載入效果動畫
========================================= */
.twikoo-skeleton {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px dashed #eee;
}
.twikoo-skeleton:last-child {
border-bottom: none;
margin-bottom: 0;
}
.twikoo-skeleton .sk-text {
width: 95%;
height: 14px;
background-color: #e2e5e7;
border-radius: 4px;
margin-bottom: 6px; /* 增加假文字的行距 */
animation: sk-pulse 1.5s infinite ease-in-out;
}
.twikoo-skeleton .sk-meta {
width: 50%;
height: 12px;
background-color: #f0f2f3;
border-radius: 4px;
margin-top: 10px; /* 把假日期往下推,增加整體高度 */
animation: sk-pulse 1.5s infinite ease-in-out;
}
@keyframes sk-pulse {
0% { opacity: 0.5; }
50% { opacity: 1; }
100% { opacity: 0.5; }
}
三、解決 Vercel 冷啟動延遲(Cold Start Latency)
Serverless 函數閒置 5~10 分鐘會進入休眠,我們透過外部定時任務強迫 Vercel 保持清醒。
1. 為什麼 GET 請求無效?
一般的監控服務使用 GET 請求,常被邊緣快取(Edge Cache)直接回傳 HTML,無法觸發底層 Node.js 執行環境。
2. 使用 cron-job.org 發送 POST 預熱
- URL:
https://comment.mynet.tw - Execution schedule:每 5 分鐘一次
- Request method:
POST - Headers:
Content-Type: application/json - Request body:
{"event": "GET_RECENT_COMMENTS", "pageSize": 1}
效果:
強迫 Vercel 喚醒 Node.js 並執行 MongoDB 查詢,將首次連線延遲從 1.5s+ 壓到 0.5s 左右。
四、機器人耗用資源試算
如果設定每 5 分鐘執行一次,一個月大約會喚醒 Vercel 伺服器 8,640 次。這數字聽起來很驚人,但對比 Vercel 與 MongoDB 的免費額度,實際消耗量連 1% 都不到,非常安全。
1. Vercel 資源消耗評估(每月)
- 執行次數:佔用不到 1%
免費上限為 100 萬次,我們僅消耗約 8,640 次。 - 運算時間(Compute):
免費上限為 Active CPU Time 4 小時(約 14,400 秒)。每次 ping 主動 CPU 時間遠低於 400ms(扣掉等待 MongoDB I/O 的時間),整月累積佔比極低。 - 網路流量:佔用約 0.01%
免費上限為 100 GB。由於每次只抓取 1 筆純文字留言,單月總流量不到 20 MB,比載入十幾張高畫質圖片還要小。
2. MongoDB 資料庫壓力評估
- 讀寫頻率:毫無壓力
免費版限制「每秒 100 次讀寫」,而我們的腳本是「每 300 秒才讀取 1 次」,完全不會拖慢其他正常留言的寫入速度。 - 傳輸流量:佔用不到 0.1%
免費版限制「連續 7 天內傳輸 10 GB」。經過計算,腳本一整個月的資料傳入與傳出總和僅約 35 MB,消耗佔比非常的低。
結論:
你可以 100% 放心掛著這隻每 5 分鐘 POST 一次的定時機器人,它不僅不會讓你超出免費額度,還能隨時維持在預熱狀態,讓留言板的內容不會延遲顯示。
五、整體方案總結
第一次訪問 → 骨架載入效果 → API 回傳後渲染 → 存入 localStorage
之後訪問 → 瞬間顯示快取舊資料 → 背景更新 → 有新留言就無縫替換
這樣即使 Vercel 偶爾冷啟動,訪客也不會看到等待時的空白畫面。而手機版 RWD 單欄布局讓側邊欄的 API 請求變成低優先級,反而不會拖慢主要內容的載入。