商协会的活动页面有个很典型的流量曲线:上线之后一周没什么人看,活动开始前两天突然涨起来,结束之后又归零。这种脉冲式访问对首屏要求很高——用户多半是从群里点链接进来的,网络环境不定,三秒还没看到内容人就走了。
我们之前的部署方式是一台轻量应用服务器上跑 Nginx,页面丢在目录里。问题有几个:带宽小,并发一上来就堵;没有 CDN,外地会员打开慢;每次发版手动传文件,容易漏。改造之后换成 COS 静态托管加 EdgeOne 加速,首屏从三秒多降到一秒出头。
先说构建。要做长缓存,前提是文件名跟内容绑定,内容变了文件名就变。Vite 默认带 hash,但别用 query 串版本号,那种方式在部分 CDN 上不会被当作不同资源。同时把 chunk 拆细,业务代码和第三方依赖分开,改业务的时候依赖包的缓存不失效。构建插件里挂上图片压缩,输出 AVIF 并保留 WebP 兜底,用 picture 标签让浏览器自己选。
图片是这类页面的大头。活动页通常有几张大图做头图,原图动辄两三兆,压完能省七八成。兼容上不用太纠结,近两年的移动浏览器基本都支持,不支持的回落到 WebP 也不会差太多。
上传环节写了个增量脚本。每次全量传一遍既慢又费请求,先拿 COS 的文件列表和本地构建产物做比对,只传哈希不一致的。
```js
const COS = require('cos-nodejs-sdk-v5');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const cos = new COS({
SecretId: process.env.TENCENTCLOUD_SECRET_ID,
SecretKey: process.env.TENCENTCLOUD_SECRET_KEY
});
const Bucket = process.env.COS_BUCKET;
const Region = process.env.COS_REGION;
const DIST = path.resolve(__dirname, '../dist');
const walk = (dir, base = '') =>
fs.readdirSync(dir, { withFileTypes: true }).flatMap((d) =>
d.isDirectory() ? walk(path.join(dir, d.name), `${base}${d.name}/`) : [`${base}${d.name}`]
);
const md5 = (p) => crypto.createHash('md5').update(fs.readFileSync(p)).digest('hex');
async function listRemote() {
const remote = new Map();
let marker;
do {
const res = await cos.getBucket({ Bucket, Region, Marker: marker, MaxKeys: 1000 });
res.Contents.forEach((o) => remote.set(o.Key, o.ETag.replace(/"/g, '')));
marker = res.IsTruncated === 'true' ? res.NextMarker : undefined;
} while (marker);
return remote;
}
async function upload() {
const remote = await listRemote();
const files = walk(DIST);
let uploaded = 0;
for (const key of files) {
const file = path.join(DIST, key);
if (remote.get(key) === md5(file)) continue;
await cos.putObject({
Bucket,
Region,
Key: key,
Body: fs.createReadStream(file),
ContentType: mime(key),
// 带哈希的资源长缓存,HTML 必须不缓存
CacheControl: key.endsWith('.html') ? 'no-cache' : 'public, max-age=31536000, immutable'
});
uploaded += 1;
}
console.log(`上传 ${uploaded} 个,跳过 ${files.length - uploaded} 个`);
}
upload();
```
这段脚本里有个必须做对的点:HTML 文件不能设长缓存。带哈希的 JS、CSS、图片可以缓存一年,但入口 HTML 每次都要回源,否则用户拿到旧入口,引用的还是旧资源,发版等于没生效。给 HTML 设 no-cache,配合 ETag 做协商缓存,既保证及时更新又不用每次传全量。
EdgeOne 这层的配置思路是:静态资源路径按后缀设长缓存并在边缘节点缓存,HTML 路径设不缓存或短缓存,回源到 COS 的静态网站域名。另外把 Brotli 压缩打开,文本类资源能再省两成体积。
发版之后还有一步容易忘——缓存刷新。带哈希的资源不需要刷,但 HTML 入口和不带哈希的老资源要主动刷新,否则边缘节点可能还存着旧内容。做法是调用 EdgeOne 的刷新接口,按 URL 提交刷新任务,只对入口路径提交即可,不用整站刷。
首屏还有一块是数据请求。活动页打开要请求活动详情,如果等 JS 加载完再发请求,链路是 HTML 到 JS 到接口再到渲染。我们改成在构建时把活动的基本信息预渲染进 HTML,首屏直接有内容,JS 加载完再接管。这一步对首屏的贡献比 CDN 还大。
改造完实测:首屏可见时间从三秒二降到一秒一,LCP 从接近四秒降到一秒五左右,活动页的报名转化率提升了一成多。带宽成本反而降了,因为大部分请求打在边缘节点上。
有几点经验值得留下。一是别把所有东西都塞进 CDN,带用户态的接口请求不能缓存,要单独走 API 域名,缓存规则里必须排除掉。二是监控要跟上,EdgeOne 的访问日志接到 CLS 之后,能看到各地区的首屏耗时分布,哪里的会员打开慢一目了然。三是回源鉴权要开,COS 开启私有读写之后只允许 EdgeOne 回源,避免源站被直接打。
我们这边在跑的商会管理系统叫未来漫城·商会互联平台,活动的报名页和邀请函都是这套部署方式,活动前临时加服务器这件事已经很久没做过了。
整体看下来,这套方案的技术含量不算高,核心价值是把"每次活动都要为性能操心"变成一次性投入。对商协会这种活动驱动、流量不平均的场景,这个投入是划算的。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。