从0到1的项目,超详细的全链路性能优化手段以及可落地的全球化 CDN 部署策略
1. CSR,SSG,SSR的区别
首先得了解前端三种渲染模式的区别,前端渲染模式的选择直接影响用户体验、开发成本和运维压力。
从体验角度考虑
-
静态内容为主:选择 SSG+CDN,兼顾速度与成本(如企业官网、帮助文档)。
-
动态内容为主:SSR > CSR > SSG,但更多是采用 混合模式,如果选SSR需权衡服务器以及运维成本。
-
轻量化交互场景:纯 CSG 即可(如简单工具类页面)
2. 我们怎么做
2.1. 决策逻辑
-
排除SSR:全球化的考虑,做SSR的对于机器运维的成本太高,跨地域访问成本更高
-
静态内容「极致优化」:用 SSG+CDN 组合拳解决「速度」与「成本」的矛盾;
-
动态内容「适度妥协」:仅依赖用户信息的强交互页面保留 CSG,进行传统的框架优化,加载时序优化,避免过度工程化。
2.2. 分析思路
项目是从0到1的过程,起初的性能指标并没有,更多是从输入url到DOM展现,思考哪一些是我们可以去优化的,整个优化过程可以抽象成三个部分,网络层加载优化,框架层渲染优化,逻辑层处理优化
2.3. 体验指标对比
2.3.1. 正常网络环境(无缓存)
结论:正常网络下,SSG 和优化后的 CSG 均能达到优秀体验,用户无明显感知上的差异。
官方的体验指标评定标准里FCP在1.8s以内就是优秀的,对比了业务中的SSG和CSG的站点,如果是在正常的网络环境下都达到了优秀的标准,从使用体感来看,加载过程中也不会感觉到慢或者明显的白屏,因此,在正常网络环境下0.5s,1s,还是1.5s并没有什么区别
2.3.2. 2.3.2 弱网络环境(以低速4G为例)
结论: 弱网络下,SSG 的优势显著 —— 只需加载静态 HTML即可展示页面, 无需等待 JS 加载再渲染,可快速展示核心内容,无白屏等待时间
在「低速4G」下, CSG性能表现截图如下,就会出现明显的白屏,而SSG不会,是秒出的体验
3. 三阶段优化
3.1. 网络层优化: CDN全球化加速的策略
核心目标:通过 CDN 配置减少网络请求耗时,提升资源加载效率。
3.1.1. 全球加速
前端构建产物,推送到OSS进行资源管理,同时通过CDN进行全球访问加速
在创建CDN时,配置域名并添加全球加速,用户访问时可以从就近的CDN节点获取静态资源,避免了每次都是从源站(OSS)进行静态资源的拉取
3.1.2. 缓存配置
- 静态资源强缓存:
-
JS、CSS、图片等静态资源设置 14 天缓存,要客户端跟随CDN缓存策略,这样资源请求后可以进行本地缓存
-
长期不变资源(如
.wasm二进制文件)配置 365 天超长缓存,避免版本未变更时的重复下载。
- HTML 动态更新:
- 不缓存 HTML 文件(
Cache-Control: no-cache),通过 CDN 规则优先级(低于静态资源)确保每次请求html获取最新页面结构,避免因缓存导致内容滞后。
3.1.3. 性能优化
-
【必选】: 开启后优化体感明显
-
HTTP/2 协议开启:利用多路复用特性,减少 TCP 连接数,提升资源并行加载效率。
-
智能压缩全开:启用 Brotli 压缩(压缩级别 6),对 HTML、JS、CSS 文件平均压缩 40%-60%,降低传输体积。
-
-
【可选】:
-
图片预处理:将大图压缩并转换为 WebP 格式(体积减少 30%+),小图标使用 SVG 或 Base64 内嵌,减少 HTTP 请求。
-
URL 参数精简:移除静态资源 URL 中的版本参数(如
style.css?v=2.0→style.css),避免 CDN 因参数差异误判缓存失效
-
3.1.4. 3.1.4 规则引擎和重定向
在CDN配置提前配置规则引擎,根据规则从CDN层面就重定向到不同页面,避免进入浏览器请求资源后再进行判断,可大幅减少跳转延时
CDN提供了两种方式来配置规则,第一种是规则脚本,第二种是规则引擎,脚本可以自行编写代码,但需要按照CDN规定的语法来写,比较麻烦,适合比较复杂的匹配逻辑,而规则引擎是CLI配置界面,适合规则明确,相对简单的配置
下面以「中英文站点的自动识别」来解释逻辑
第一,添加规则引擎规则:「重定向到中文站」和「重定向到英文站」, 主要是两条规则
-
如果用户选择过语言环境,优先使用用户选择的语言环境
-
如果用户未选择过语言环境,根据用户IP所在地理位置确定语言环境
第二,配置重写访问URL,这里要注意的是执行规则一定要选择break,break的意思是匹配到这一条规则后,下一次进入不会再匹配其他规则,可避免反复重定向
3.2. 3.2 框架层优化: 混合渲染实践(CSG+SSG)
核心策略:静态内容 SSG 预渲染,动态内容 CSG 客户端渲染,平衡体验与开发成本。
有登录态依赖的页面使用CSG,无登录态依赖的页面使用SSG,比如登录页面,协议页面
为什么不是所有页面都SSG?
不是不能做,只是前端性能优化的实践中,还是得结合项目的实际情况进行选择。当时我们做完其他性能优化改造后,整体页面性能已经很好了,再去把剩余页面改造成SSG收效比较小,因此就没有投产, 但也做过一些技术验证过可行性。
3.2.1. SSG 项目改造
在 Nuxt 中实现 SSG 改造可分为配置调整与逻辑适配两大步骤, SSG的改造适用于混合渲染模式以及纯SSG渲染模式,三个项目都是用的以下方式
3.2.1.1. 配置改造: nuxt.config.ts
SSG 的核心是在构建阶段生成静态 HTML 文件,Nuxt 提供了灵活的配置方式,可根据路由需求精细化控制预渲染范围。配置的核心逻辑是:通过 ssr 启用服务端渲染能力,通过 prerender 指定需要生成静态文件的路由。
-
ssr: true:允许该路由在构建 / 请求时执行服务端逻辑(预渲染的前提)。 -
prerender: true:指定该路由在构建阶段生成静态 HTML 文件(SSG 核心)。 -
未配置的路由默认继承全局
ssr配置,可按需组合实现 “部分页面静态化、部分页面动态化”。
可参考 Nuxt 官方文档 实践,以下是两种常用配置方案:
方案一: 预渲染 + 自动爬取链接(适合内容关联紧密的站点)
export default defineNuxtConfig({
// 开启服务端渲染能力(预渲染依赖服务端逻辑)
ssr: true,
nitro: {
prerender: {
// 从根路由开始爬取
routes: ['/'],
// 自动爬取页面中的 链接并预渲染
crawLinks: true,
}
}
})
方案二: 路由规则精准控制(适合混合渲染场景)
通过 routeRules 为不同路由单独配置渲染策略(SSG/SSR/CSR),灵活度更高:
export default defineNuxtConfig({
// 全局开启服务端渲染能力
ssr: true,
// 路由规则:配置混合渲染模式
routeRules: {
// 预渲染的页面(SSG)
'/login': { ssr: true, prerender: true },
'/agreement': { ssr: true, prerender: true },
...
// 客户端渲染(CSG)
'/main/**': { ssr: false, prerender: true },
....
},
})
3.2.1.2. 逻辑改造: 兼容客户端特有依赖
预渲染的本质也是执行的服务端渲染,但部分代码(如依赖 window、document 等浏览器 API)只能在客户端运行。若直接执行,会导致构建报错或页面异常,需通过以下方式区分客户端 / 服务端逻辑
客户端逻辑隔离: onMounted 与 import.meta.client
-
onMounted钩子:在 Nuxt 中,onMounted内的代码默认仅在客户端执行,适合初始化依赖浏览器 API 的逻辑(如 DOM 操作、事件监听): -
import.meta.client判断:通过环境变量显式区分客户端逻辑,适合非生命周期内的代码:export const useDevice = () => { // 只在客户端执行检测,SSG构建的时候会忽略这一段代码 if (import.meta.client) { isMobile.value = checkIsMobile(); } }
客户端组件隔离: 组件
对于完全依赖客户端环境的组件(如包含 window 操作的第三方组件),可使用 Nuxt 内置的 <ClientOnly> 组件包装,确保其仅在客户端渲染:
混合渲染:指定逻辑使用SSG,CSG,精细化控制页面的预渲染或者组件的预渲染
-
nuxt.config.ts需要先改造成SSG渲染的配置
-
对页面局部内容控制渲染方式,通过指定组件进行CSG渲染,(如顶部和侧边菜单 SSG + 主体内容 CSR)
3.2.2. CSG 资源加载优化
主要的分析手段有两个
-
使用chrome的Performance分析资源加载情况与白屏问题分析,使用Lighthouse进行FCP分析
-
基于Nuxt的Analyze并结合Cursor进行首屏资源的拆包分析和优化,首屏CSS从200KB -> 40KB, 入口JS从458KB,拆为三个150KB并行加载
3.2.2.1. 非首屏资源动态加载
非首屏资源,使用import实现动态加载,包括G6,Markdown解析依赖
3.2.2.2. 图片优化
将大图压缩并转换为 WebP 格式(体积减少 30%+),并放到CDN上进行管理
小图标使用 SVG 或 Base64 内嵌,减少 HTTP 请求。
3.2.2.3. 关键资源进行拆包优化
在nuxt项目里,本身自带了一些拆包优化以及tree-shaking的策略,比如
-
从app.vue里的进入的代码都会打到入口文件,entry.js,每个路由文件都会有一个单独的js文件
-
项目里引用的组件库自带tree-shaking,entry或者路由文件都只会加载用到的组件代码
因此,代码分割的主要策略是
-
将入口文件entry.js进行拆包优化,将不常用的lib库单独抽出来,目的是并行加载提高速度,同时利用好CDN的缓存优势,避免每次打包都产生新的hash,导致每次都是重新加载
-
针对阻塞性CSS进行分析,删除冗余样式
第一步,使用cursor进行拆包分析
在执行打包时,利用cursor将每个文件依赖的chunk以及chunk大小都打印出来写到file.analyze.log里,然后进行包分析,确定拆包范围
第二步,nuxt.config.ts添加vite的构建配置,通过manualChunks进行拆包
export default defineNuxtConfig({
...,
vite: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
// Vue 核心库
if ([...vueLibs].some(lib => id.includes(lib))) {
return 'vue-lib'
}
// 国际化库
if ([...i18nLibs].some(lib => id.includes(lib))) {
return 'i18n-lib'
}
// Tailwind 相关
if ([...tailwindLibs].some(lib => id.includes(lib))) {
return 'tailwind-lib'
}
}
},
},
},
}
})
第三步,nuxt.config.ts中通过hooks修改html中的chunk文件的加载顺序,让lib相关的文件提前加载放到entry之前
export default defineNuxtConfig({
...,
hooks: {
'nitro:build:public-assets': (_nitro) => {
// 修改html文件中JS的加载顺序
}
})
3.3. 逻辑层优化: 用户体验的细节打磨
核心目标:消除加载卡顿,提升交互流畅度,强化设备兼容性。
3.3.1. 登录页整出
登录页面是预渲染的,单个html文件是4kb,整体渲染很快,但登录页面有一些图片,一开始使用的是CDN,虽然已经有缓存优化了,但毕竟是网络请求,而且背景图很大,有70KB,访问登录页的时候背景图总是会有个请求过程再出现,体验不是很好。
因此,针对登录页的优化是
-
页面SSG整出
-
依赖图片全部用base 64加载,与html一起加载
优化后,html虽然增加到了80KB,但首次访问时不会出现图片加载延迟的情况,整体页面呈现更为流畅
3.3.2. 登录态校验前置
登录态校验原先是在Nuxt的中间件里执行,但就需要entry和lib的相关JS加载完后,才能执行,这势必会导致首次访问的速度变慢。
因此,针对登录态校验的逻辑前置到所有逻辑前面,单独提成一个JS文件,放到所有JS前面,并内敛到html里,随html一起下发并执行
-
设置async进行异步执行,避免阻塞html渲染
-
要求后端提供一个单独的checkToken接口提升整体响应速度,从原先的50ms提升到18ms
-
在中间件中判断校验结果,并增加兜底逻辑
在这18ms的响应过程中,并行做了两件事,一方面是执行了登录态校验,另一方面也是执行了渲染逻辑
// 全局JS
(function() {
// 创建认证完成的Promise
window.__CHECK_AUTH_READY__ = new Promise((resolve) => {
window.__RESOLVE_AUTH__ = resolve
})
function checkAuthStatus() {
// 1. 请求后端接口,执行校验逻辑
...
// 2. 请求成功
window.__RESOLVE_AUTH__(true)
// 3. 请求失败
window._RESOLVE_AUTH__(false)
}
})()
// middleware/auth.ts
export default defineNuxtRouterMiddleware(async () => {
if (!import.meta.client) {
return
}
const isAuthenticated = useAuth()
isAuthenticated.value = await Promise.race([
window.__AUTH_READY__,
new Promise(resolve => {
cont time = setTimeout(() => {
// 兜底逻辑,1.5s没有响应进行重新校验
}, 1500)
})
])
])
3.3.3. SpaLoadingTemplate 设置骨架屏
部分页面采用的是CSG渲染,不管优化到什么程度,客户端渲染势必需要等待JS加载完成再进行渲染,,首次加载或者是弱网环境下,依然会有个卡顿现象,因此利用Nuxt的SpaLoadingTemplate设置骨架屏
4. 总结
按优先级和收效进行排序
-
网络优化: 充分利用CDN的能力,收效最大,全球加速 | 缓存配置 | 资源压缩 | 规则引擎+重定向
-
**渲染模式:**SSG预加载体验,在弱网模式下以及首次加载远远好于CSG
-
动态加载: 非关键资源动态加载,大幅减少首屏加载js大小
-
代码分割: 不常用的css和js要抽离出来,利用缓存优势,避免每次打包都产生新的hash,重新加载
-
逻辑优化: 骨架屏以及一些体验细节的优化
优化的核心原则
-
能预渲染的,就预渲染
-
利用缓存优势,优先客户端强缓存,其次CDN缓存,最后no-cache进行协商缓存
-
利用CDN进行前置处理进行重定向的判断,避免进入客户端再去判断
-
不能有白屏,不能有闪烁,不能有超出用户预期的体验
技术方案的选择本质是对「用户体验」「开发成本」「场景适配性」的综合权衡,得结合项目的实际情况进行选择。脱离具体场景的「极致优化」,往往是技术炫技而非真实需求了。就像我们在项目中纠结于将 FCP 从 0.6s 压到 0.5s 时,更该思考:这个差值对用户而言,真的体验的出来吗。
因此,性能优化还是要回归用户体验的本质,当用户打开页面时,不会注意到 SSG 如何预渲染 HTML,也不会关心 CDN 节点离自己多近,他们只会觉得「页面自然而然就出来了」,当他感受不到性能时才是好的体验,就会专注在产品功能本身,这种「无感知的流畅」,才是体验的终极目标。