前言:
在互联网行业步入存量运营的新常态下,企业同时面临用户留存与获客增长的双重挑战,当推广运营、付费转化等其他因素保持不变时,用户体验已成为驱动用户增长的核心变量。对于一个拥有亿级用户规模的会员体系而言,用户体验更是是关键所在。本文将深入探讨亿级用户会员体系的性能优化历程,旨在为行业同仁提供可借鉴的经验和启示。
本文的结构如下: 第1部分阐述前端性能对用户体验的重要性、性能度量体系,以及顺丰会员性能的标准和要求。 第2部分是文章核心,系统总结性能优化的四类策略,其中2.4小节着重介绍SSR和SPA应用的预渲染技术。 第3部分展示性能优化前后的效果对比。 第4部分对全文进行总结,强调性能优化是一个持续迭代、循序渐进的过程。
1. 直面性能困局:亿级会员的体验挑战
1.1 背景
⚠️对于前端来说,页面加载时间,直接影响用户的体验,以及用户的留存和转化。
调研报告显示:
- 75%的人认为:崩溃率、页面加载时长是影响一个APP是否有吸引力的核心因素。
- Pinterest:减少页面加载时长40%, 提高了搜索和注册数15%;
- BBC:页面加载时长每增加1秒,用户流失10%;
- DoubleClick:如果移动网站加载时长超过3秒,53%的用户会放弃。
1.2 性能度量指标
⚠️如果一个事物无法被衡量,那么就无法被优化。
前端的性能优化指标,以谷歌定义[参考1]为主流参考标准:
LCP(Largest Contentful Paint,最大内容绘制)、INP(Interaction to Next Paint,交互到下一次绘制)和 CLS(Cumulative Layout Shift,累计布局偏移)作为其中的核心指标,全面衡量着前端性能表现。
其中,LCP 是衡量用户留存的关键指标,它记录着从页面开始加载到最大文本块或图片等主要内容渲染完成的时间,直观反映页面的打开速度,下文主要以LCP作为性能优化度量基准讨论。
1.3 顺丰会员前端性能挑战
在顺丰会员体系庞大的用户生态中,已汇聚亿级规模用户,每日活跃用户超千万。如此庞大的用户体量,使得前端性能成为维系用户体验、保障业务高效运转的关键命脉。
按照谷歌提供的标准,我们应该制定一个什么样的性能基线呢?
首先来分析一个经典页面流:
①②③④步主要逻辑是鉴权获取微信信息,然后重定向到web前端,红色方框的时间是前端渲染的时间,这个时间显的过长,是首要优化的内容。
那么,从重定向/homeInner到LCP阶段都经历了什么?
通过抓包(Chrome Dev Tools)我们能看到资源加载情况、接口请求时长等关键信息,可以对前端的性能问题进行深入分析。
同时,我们也使用Chrome Lighthouse[参考2]进行检测,给出了一些优化建议:
如上是性能问题定位一瞥,可以快速判断前端性能是否有问题,以及问题有多大。按照上面提供的信息,判断出性能有待于优化:
# 性能检测结果
LCP: 2.1s | CLS: 0.399 | TTI: 2.5s
有时候用实验室数据(性能检测工具)会觉得网站性能还行,那真实的情况是什么样的?此时,就需要权威专业的数据统计能力进行数据埋点,获取用户真实的性能情况。
常见的埋点方法有以下两种:
埋点方式 | 特点 | 适用场景 |
web-vitals | 谷歌官方出品 | 适用于所有标准场景 |
自定义上报 | 需要用PerformanceObserver监听性能的关键事件 | 适用于需要特殊处理,比如存在延迟弹窗等场景 |
1.使用谷歌的web-vitals库:
import { onLCP } from 'web-vitals'
onLCP((data: Metric) => {
// 上报LCP到埋点
sendEvent.wvLCP(data.value)
})
2.可以自定义上报:
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP元素:', lastEntry.element);
console.log('LCP时间:', lastEntry.startTime);
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
Tips: 此种方式上报,可以灵活上报,lcp会多次触发,对于一些特殊场景(比如等待1s出弹窗),个性化计算LCP可能更准确。
针对于常规页面,我们选择方案1做埋点,针对大促活动页面,我们选择方案2自定义上报LCP的方式。
2. 性能优化四部曲
在进行性能优化之前,先看一个页面请求的全过程:
在这个链路的各个阶段,都有可能影响页面的加载速度,影响着前端的性能。
Tips:在这个请求过程中省略了WAF、LB、负载等链路,如果需要更深入的了解,可以期待后续的技术文章。
性能优化某种程度可以考虑火车运力的情况:
要想加载时间最小,改变s和v的某一单项,都能达到目标。
总体来说,可以按照如下四个维度进行性能优化:
在性能优化过程中,我们采用了多层次的技术手段,并按照优化内容进行了系统归类:
- 基础优化阶段 项目初期,通过常规优化手段(如2.1-2.3节所述)提升了前端性能。
- 高阶优化阶段 当常规优化达到瓶颈后,引入预渲染和SSR技术(2.4节),进一步突破性能上限,优化效果显著提升。
⚠️方案无优劣,效果好才是王道。
2.1 资源瘦身:降低资源大小
- 图片压缩
使用 TinyPNG 等专业压缩工具,基于 MozJPEG、Zopfli PNG 等算法对图片进行有损压缩与无损优化。
以电商类会员页面为例,一张 500KB 的商品图片经 TinyPNG 压缩后,体积可降至 150-200KB,视觉损耗控制在 5% 以内,图片大小能减小60%以上。
同时,TinyPNG支持API调用,项目中可工程化集成,进行批量图片处理。
- 图片格式选型
图片格式的选择对于前端性能也至关重要,以下为主流的图片格式:
格式 | 压缩类型 | 压缩率 | iOS支持 | Android支持 | 动画支持 |
JPEG/JPG | 有损压缩 | 基准(100%) | 全版本 | 全版本 | ❌ |
PNG | 无损压缩 | 约 30-50%(比 JPEG 大) | 全版本 | 全版本 | ❌ |
WebP | 有损 / 无损 | 65%(有损) | 14.0+ | 4.2+ | ✅ |
AVIF | 有损压缩 | 73% | 16.4+ | 12.0+ | ✅ |
HEIC/HEIF | 有损压缩 | 50-75% | 11.0+ | 9.0+(需插件) | ❌ |
webp由于其优秀的压缩率和兼容性,得到了广泛使用。如下是整个webp的工程化方案,其中对兼容性做了考虑:
当然目前云服务商也都提供了webp的自动转换,比如腾讯云的CDN的图片优化配置[参考3]的服务。
- 资源压缩
除了图片,前端资源应尽可能压缩,减小传输大小,同时删除项目中无用资源、代码。
2.2 精简数量:火车少拉一些
- 图片、模块懒加载
图片懒加载:通常做法是,当图片出现在视窗位置的时候才进行加载。
<img data-src="real-image.jpg" src="placeholder.jpg" alt="Lazy Image">
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));
组件的懒加载:使用框架特性,对资源拆分、异步处理。
import React, { lazy, Suspense, useState } from 'react';
const LazyModal = lazy(() => import('./LazyModal'));
// ...路由配置
- Service Worker降低请求
通过 PWA 技术使用 Service Worker实现离线缓存,提升网络不稳定情况下的可用性。
- 资源缓存
静态资源长缓存,使用hash命名策略处理文件更新。
// webpack配置contenthash
module.exports = {
output: {
filename: 'js/[name].[contenthash].js', // JS 文件命名
assetModuleFilename: 'images/[name].[contenthash][ext]' // 图片等资源命名
},
plugins: [
new CleanWebpackPlugin(), // 清除旧资源
]
};
# 在nginx设置1年的长缓存时间
location ~* \.(css|js|json|jpeg|jpg|webp|png|mp3|gif){
add_header Cache-Control "max-age=315536000"
}
但是需要注意的是,对于单页应用的index.html资源是不能够缓存的,避免引用旧资源,页面无法更新。
2.3 传输优化:提升网络效率
- 静态资源CDN
动态加速网络拓扑图,选用优秀的CDN服务提供商加速节点。
- DNS解析优化
基于用户地理位置的DNS解析优化策略。
# 智能路由配置示例
geo $nearest_cdn {
default oss-cn-shenzhen;
113.*.*.* oss-cn-guangzhou;
61.*.*.* oss-cn-shanghai;
}
- 协议升级:HTTP2/3实战解析
协议的提升能够极大的提高页面的加载速度,是页面性能的利器。得益于多路复用、二进制分帧以及UDP(QUIC)传输协议,极大提高了请求效率,如下是技术对比:
特性 | HTTP/1.1 | HTTP/2 | HTTP/3 |
传输层协议 | TCP | TCP + TLS | UDP(QUIC)+ TLS1.3 |
队头阻塞 | 连接级:单个 TCP 连接中所有请求串行 | 连接级(单个 TCP 连接中) | 流级(单个 QUIC 流中) |
握手延迟 | 1-RTT | 2-RTT | 1-RTT(首次连接),0-RTT(重复连接) |
弱网络表现 | 差(TCP 重传机制在丢包率高时延迟显著) | 较差(依赖 TCP 重传) | 优秀(多路复用 + 快速重传) |
浏览器支持度 | 全兼容 | 广泛支持 | 主流浏览器已支持 |
服务器部署复杂度 | 低(全支持) | 低(多数 Web 服务器支持) | 较高(需 QUIC 支持) |
下图简单对比HTTP/1.1和HTTP/2.0的加载速度对比,可以看出HTTP/2.0多路复用技术能够并行接口请求,提高了请求速度。
HTTP/1.1
HTTP/2.0
HTTP/2.0开启:
# nginx开启h2
server {
listen 443 http2 ssl;
server_name_in_redirect off;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
ssl_protocols TLSv1.2 TLSv1.3; # 推荐仅启用 TLS 1.2+
server_name localhost;
location / {
// ...
}
}
- dns-prefetch
前置解析dns,减少查找dns时长。
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 其他配置 -->
<link rel="dns-prefetch" href="//a.com">
<link rel="dns-prefetch" href="//b.com">
</head>
<body>
</body>
</html>
2.4 渲染革命:提升执行效率
- spa应用资源preload并行加载
大多数SPA应用,在加载页面的时候,都是先加载公共资源,再根据路由匹配异步chunk,这是一个串行过程,对于重要的页面,如果能够并行加载资源就能更快的打开网页。
⚠️关键点:找到异步chunck的依赖关系,注入到index.html中。
此种方案将单独生成一个html文件,比如homeInner.html,当请求到home路由的时候,就返回此html页面,里面包含同步和异步chuncks,可以并行加载,更快打开页面。
大致思路如下(拿用户访问/homeInner路由为例):
1.遍历所有chunks 可以得到 mapA
{'@views/home-page-new/HomePageNew': ['/static/js/chunk-a.hash.js', '/static/js/chunk-b.hash.js'...]...}
2.解析app.js中路由表AST 可以得到mapB
{'/homeInner': ['chunk-a', 'chunk-b']}
3.整合mapA mapB 得到path和资源的映射关系
{'/homeInner': ['/static/js/chunk-a.hash.js', '/static/js/chunk-b.hash.js']}
4.读取index.html 头部插入preload信息 写入文件
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 其他配置 -->
<link rel="preload" as="javascript" href="/static/js/react.js">
<link rel="preload" as="javascript" href="/static/js/chunk-a.hash.js">
<link rel="preload" as="javascript" href="/static/js/chunk-b.hash.js">
</head>
<body>
<!-- 其他配置 -->
</body>
</html>
5.配置nginx,设置反向代理,重新匹配文件
set $html_file_name $uri;
location / {
root html;
index index.html index.htm;
try_files $uri $uri/ @try_files_fallback;
}
location @try_files_fallback {
try_files $html_file_name.html $uri.html /index.html;
add_header Cache-Control "no-cache,no-store";
}
利用预加载加载手段,可以降低30ms-60ms的加载时长。
- 服务端渲染SSR
⚠️对于缓慢的网络情况或运行缓慢的设备,SSR无需等待所有的JavaScript都完成下载并执行,用户能够更快速地看到完整渲染的页面,提升用户体验。
如下是一个SSR渲染的通用流程,核心是以服务端的计算能力换取客户端的加载时间:
SSR渲染的核心流程
SSR也可以做渐进式注水策略,如下是一个简单案例。
// 渐进式注水策略
const hydrateRoot = createRoot(container, {
hydrate: true,
hydrationOptions: {
onHydrated: () => {
// 延迟加载非关键组件
requestIdleCallback(loadSecondaryModules)
}
}
});
在做ssr渲染时,我们尤其需要需关注的是灰度和降级方案等,如下是一个简单的架构图,包含降级和部署方案。
一个简单的降级方案
SSR的优化效果对于我们资源加载效果明显,具体对比可以参考第3小节。
当然,如果对于SSR效果仍然不满意,可以更近一步使用SSG、ISR技术进行优化,打开速度更快。
- 接口聚合
可以使用bff层,对多个接口进行聚合优化请求,减少接口请求次数以及请求负载。
3. 优化效果对比
⚠️性能优化是一个持续的过程,团队历经两年多的时间,优化效果明显。
常规方案优化效果:
会员中心页面(LCP)加载2.5S -> 1.2S时间优化了52%。全站页面(LCP)加载速度提升了28%。
会员中心秒开率:优化前秒开率不高于10%,优化后秒开率51%,提升了400%。
加载优化历程
秒开率
当常规优化遇到一定瓶颈之后,我们引入SSR方案,引入SSR之后:
1. SSR的LCP是766ms,非SSR(CSR)加载时间是1.3238s,加载速度提升73%。
2.会员中心CSR前秒开率为51%,SSR会员中心秒开率为80%。
CSR VS SSR
秒开率
4.总结
对于C端系统,特别是亿级用户体系,用户体验一直都是一个重要的话题,前端性能关切着产品的体验和用户的增长。我们从指标度量、埋点到一系列技术手段的优化,沉淀了完整的方法论。总体来说,我们用火车的案例来说明性能优化的四个方向:
- 资源瘦身:降低单个资源的大小,选择合适的图片格式以及资源压缩等
- 精简数量:减少请求数量,懒加载或者缓存等
- 传输优化:采用更快的网络,开启HTTP/2.0或者HTTP/3.0等
- 渲染革命:采用预渲染以及SSR等技术提高渲染效率
性能优化不是一蹴而就,也不是一次性优化的,它是需要持续优化精进的过程,从性能采集、优化、监控是一整套体系的过程。
附录
参考链接: