VoociiBlog
HomeBlogAI TrendingPortfolioBooksLinksToolsAbout

© 2026 Voocii. Built with Next.js & tRPC.

GitHubXEmailRSS


个人博客性能调优实战:0ms 瞬间加载

Tutorialrick-hayekrick-hayek2026年7月14日

作为一个个人博客系统,内容的更新频率通常相对较低(可能几天更新一次甚至更久)。在这样的场景下,我们该如何将 Next.js 博客的性能榨干到极致?

如果你的目标是将博客部署到 Cloudflare Pages / Workers 等具有严格代码包大小限制(例如免费额度压缩后 3MB)的边缘计算平台,体积和速度的平衡就显得尤为关键。

本文将结合本站的重构实践,分享五项极具实操价值的 Next.js 性能调优方案。


1. 客户端二级缓存:基于 LocalStorage 的 SWR(Stale-While-Revalidate)方案

Next.js 的 React Server Components(RSC)和服务器缓存非常优秀,但对于需要与服务器通信的客户端渲染(Client Component)列表(例如:友情链接、评论区、AI 趋势板、作品集筛选等),每次切换页面或重新载入时都会出现短暂的白屏或 Loading 骨架屏。

为了实现 0ms 瞬间加载,我们可以叠加一层本地 localStorage 缓存,采用 SWR(先旧后新) 策略:

  1. 首次打开:前端正常请求服务端,拿到数据后在页面渲染,并同步存入浏览器的 localStorage 中。
  2. 再次打开:立即读取 localStorage 并将其渲染到屏幕上,实现 0 毫秒感知。同时,在后台静默向服务器发起请求进行校验。
  3. 后台更新:如果服务器返回的最新数据与本地缓存一致,则前端无感知;如果有更新,则静默更新本地缓存,并动态更新页面内容。

自定义 React Hook 实现:useLocalSWR

我们可以封装一个通用的 Hook 来处理这个逻辑,同时做好 React 在服务端渲染(SSR)时的水合防错(Hydration Safety):

'use client';

import { useState, useEffect } from 'react';

export function useLocalSWR<T>(
  cacheKey: string,
  fetcher: () => Promise<T>,
  options = { revalidateOnMount: true }
) {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);
  const [isValidating, setIsValidating] = useState(true);

  // 1. 客户端挂载后,立即恢复本地缓存
  useEffect(() => {
    if (typeof window === 'undefined') return;
    
    const cached = localStorage.getItem(cacheKey);
    if (cached) {
      try {
        setData(JSON.parse(cached));
      } catch (e) {
        localStorage.removeItem(cacheKey);
      }
    }
  }, [cacheKey]);

  // 2. 静默发起请求校验并更新
  useEffect(() => {
    let active = true;

    async function revalidate() {
      setIsValidating(true);
      try {
        const freshData = await fetcher();
        if (!active) return;

        const freshString = JSON.stringify(freshData);
        const cachedString = localStorage.getItem(cacheKey);

        if (freshString !== cachedString) {
          setData(freshData);
          localStorage.setItem(cacheKey, freshString);
        }
      } catch (err: any) {
        if (active) setError(err);
      } finally {
        if (active) setIsValidating(false);
      }
    }

    if (options.revalidateOnMount) {
      revalidate();
    }

    return () => {
      active = false;
    };
  }, [cacheKey, fetcher, options.revalidateOnMount]);

  return { data, error, isValidating };
}

2. 移除 rehype-highlight 默认全语言包,精简 1.5MB+ 压缩体积

许多人写技术博客会使用 rehype-highlight 做代码高亮。但默认的 rehype-highlight(基于 Highlight.js)会在打包时静态引入所有 190 多种编程语言的解析语法包。

这会导致你 Next.js 构建出来的 Node.js/Edge Server Bundle 体积瞬间膨胀几兆,从而无法部署到 Cloudflare 免费版。

解决方案:基于 lowlight 定制轻量高亮插件

既然是个人博客,我们常用的编程语言大概只有 10 种左右(例如:JS、TS、Rust、Python、Go、SQL、HTML/CSS、Bash、Markdown、JSON、YAML)。我们完全可以写一个自定义的 Rehype 插件,只静态注册我们需要的这几种语言,从物理上避开其它 150 多种语言的打包。

在你的 lib 目录下新建 rehype-custom-highlight.ts:

import { createLowlight } from 'lowlight';
import javascript from 'highlight.js/lib/languages/javascript';
import typescript from 'highlight.js/lib/languages/typescript';
import python from 'highlight.js/lib/languages/python';
...

// 仅注册博客用得到的常用语言
const lowlight = createLowlight({
  js: javascript,
  javascript,
  ts: typescript,
  typescript,
  py: python,
  python,
  ...
});

// 手写简单的 HAST 树递归遍历,避免引入额外的 node_modules 辅助库
function walk(node: any, callback: (node: any, parent: any) => void, parent?: any) {
  callback(node, parent);
  if (node.children && Array.isArray(node.children)) {
    for (const child of node.children) {
      walk(child, callback, node);
    }
  }
}

export default function rehypeCustomHighlight() {
  return function (tree: any) {
    walk(tree, (node: any, parent: any) => {
      if (node.tagName !== 'code' || !parent || parent.tagName !== 'pre') return;

      const className = node.properties?.className || [];
      const langClass = className.find((cls: any) => typeof cls === 'string' && cls.startsWith('language-'));
      const lang = langClass ? langClass.slice(9) : null;

      if (!lang) return;

      try {
        const text = node.children[0]?.value || '';
        const result = lowlight.highlight(lang, text, { prefix: 'hljs-' });
        if (result.children && result.children.length > 0) {
          node.children = result.children;
        }
        if (!node.properties.className.includes('hljs')) {
          node.properties.className.unshift('hljs');
        }
      } catch (err) {
        // 遇到未知语言静默跳过高亮
      }
    });
  };
}

将 MDXRemote 或编译流水线中的 rehypeHighlight 替换为上面的 rehypeCustomHighlight,打包体积即可降 1.5MB+ Gzip 压缩体积!


3. 重型依赖包调优法则:寻找轻量级替代、CDN 混载与按需自实现

在 Next.js 服务端开发(尤其是在部署到 Cloudflare Workers 等 Edge runtime 资源限制严格的环境)中,引入第三方功能包是导致构建体积超限的主因。如何系统性地瘦身庞大的第三方依赖?可以遵循以下四项通用的精简法则:

A. 寻找轻量级替代品 (Alternative Finder)

在引入大包(例如富文本编辑器、数学公式排版、三维渲染等)之前,先通过 bundlephobia 检查其打包体积。

  • 法则:如果一个包的体积已经超过 100KB (Gzip),检查是否有功能精简但极度轻量的替代方案。例如,用轻量级的自定义编译器取代厚重的编译排版插件;或者用只针对单功能的微型库代替全能庞大的通用大包。

B. 服务端与客户端分工协作 (Server-Client Coordination)

有些第三方重型包的工作可以拆分为“结构解析”与“渲染排版”两部分:

  • 服务端(解析防爆屏蔽):在服务端只引入轻量级的词法解析插件(通常只有几 KB)。它仅仅在渲染前对源 Markdown 或 HTML 文本做语法标记和分词,并转换为普通的 HTML 自定义类名(如 <span class="data-node">...</span>),借此防止渲染引擎把特殊符号错误地解析为 JS/JSX 表达式,避开 Acorn 等编译器崩溃的问题。
  • 客户端(真正编译计算):重型的排版计算逻辑完全由客户端的 <ScriptRenderer /> 组件异步承接。

C. CDN 外部混载方案 (CDN & Local Hybrid)

为了彻底将重型引擎从本地打包中剥离:

  • 法则:不要用 import 将引擎 JS/CSS 静态打包进项目。而是仅在需要的动态页面中,通过 HTML 的 <script defer src="https://cdn..." /> 异步加载外部 CDN 上的公共资源。
  • 客户端配合路由监听: 在 Next.js 的 SPA 路由切换时,需要在客户端写一个轻量组件,挂载 React 的 usePathname() 钩子。只要监听到路由改变,就在浏览器中自动调用已从 CDN 加载完毕的 window.someHeavyEngine.render(node) 方法。这种“按需按时”初始化大大解放了服务端体积,且对首屏冷启动极其友好。

D. 微小功能本地自实现 (DIY over Import)

很多时候我们为了使用一个大包中不到 5% 的特定小功能,就不惜把整个包全量导入。

  • 法则:评估该功能的实现复杂度。如果是简单的 AST 遍历、树节点提取或常用的正则过滤,完全可以在本地手写几行代码解决。
    • 例如:如果主项目里只需要在大包里实现一个 HTML 节点的遍历,与其额外引入一个第三方辅助遍历库,不如直接在本地手写一个 10 行以内的递归 walk 函数,这能省下几十甚至上百 KB 的第三方链条包体积。

4. 彻底剥离静态资源,全站样式外部 CDN 化

在传统开发中,我们会直接通过 npm 安装代码高亮样式表或 KaTeX 字体包,并通过 @import 导入到 globals.css 中。然而,在边缘 Worker/Edge 环境中,这些字体资产和外部 CSS 在被编译进 Next.js 构建体积时,会极大增加冷启动的负担。

  • 优化前:globals.css 中 @import "highlight.js/styles/atom-one-dark.css" 导致构建时 PostCSS 将全套样式打包进主要 CSS 文件。
  • 优化后:仅在需要的页面(如文章详情页)上直接引入高亮和数学样式。
<link
  rel="stylesheet"
  href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css"
/>

这保证了全站其他页面(如首页、关于页)在加载时不需要承担这部分无关的代码负担,实现了资源按需最小化加载。


优化成效总结

通过对 Next.js 流水线的全方位重构,我做到了:

优化项服务端包体积减少客户端加载首屏速度 (二次访问)部署限制兼容性
移除默认 rehype-highlight~1.5 MB无变动极其友好
KaTeX 服务端转客户端~400 KB无变动极其友好
RSC 配合客户端 localStorage SWR无变动从 ~400ms 降至 0ms友好

现在,个人博客在 Next.js 的高自由度配置下不仅能够实现极致的代码瘦身,还能在秒开表现上向纯静态生成的博客(Gatsby/Hugo)看齐,甚至拥有更流畅的后台静默校验及无感刷新体验。

Comments (0)

No comments yet. Be the first!

On this page
  • 1. 客户端二级缓存:基于 LocalStorage 的 SWR(Stale-While-Revalidate)方案
  • 2. 移除 rehype-highlight 默认全语言包,精简 1.5MB+ 压缩体积
  • 3. 重型依赖包调优法则:寻找轻量级替代、CDN 混载与按需自实现
  • 4. 彻底剥离静态资源,全站样式外部 CDN 化
  • 优化成效总结