背百遍八股不如一次实操。
今天中午12点34分上线了一个h5活动页,2核、4G、5M的服务器在160个同时连接数时造成了静态资源加载极慢、按钮点击无效的问题。因为很紧急,所以看到是带宽限制之后果断选择提高上限至100Mbps,包年付费改为流量计费。
但要彻底解决问题还需要就此问题进行复盘,并
结合大家耳熟能详的八股进行实操。
一、服务器配置(2核4G 5M)分别决定了什么?
最开始,我们必须了解一个服务器的配置中,CPU核心数、运行内存、带宽分别决定了什么,这样才能对症下药。
1.1 CPU核心数
- 影响并发处理能力:核心数越多,可同时处理的任务越多,适合 Web 服务、高并发接口等。
- 影响系统性能与稳定性:核心数不足会导致任务排队、系统卡顿,甚至宕机。
1.2 运行内存
- 决定可运行的服务数量与类型:内存不足会导致服务无法启动或崩溃,影响如 Redis、Java 等内存依赖型应用。
- 影响并发请求处理能力与大任务支持:内存不足时难以应对高并发或大文件处理,系统易卡顿或报错
1.3 带宽
都知道带宽决定了网速,但是有几个细节必须注意:
1.3.1 上行带宽决定了上传速度,下行带宽决定了下载速度。
- 家庭带宽
网络运营商考虑到一般家庭下载的需求大于上传。所以300 Mbps的带宽,通常,下行带宽 可能会在 200-300 Mbps 之间,而 上行带宽 可能只有 50-100 Mbps,具体的分配比例取决于你所在的宽带服务提供商。(贴一篇中国电信的小科普:宽带网速(下载网速/上传网速)相关知识)
- ECS带宽
从文章开头的带宽监控图就可以看出, 5Mbps 的ECS带宽,最终下载的网速就是 5Mbps,同时上传网速能达到10Mbps(虽然这个h5中没有上传功能)。也就是说,ECS带宽和家庭带宽不一样,ECS带宽不是上行下行总和,而是指出网带宽(下载)。那ECS的入网带宽(上传)怎么计算?我直接贴2张图:
1.3.2 整个网络传输的速度是源端带宽、目的端带宽、中间网络路径带宽的最小值。
- 下载时,源端是服务器,目的端是本机电脑;上传时,源端是本机电脑,目的端是服务器。中间的网络硬件设备也都会有带宽限制。最终的网速是网卡、路由器、家庭宽带、服务带宽等等的最小值。
(几年前我家宽带升级300Mbps之后一直没体会到该有的速度,后来才发现路由器还是100Mbps的...)
1.3.3 多设备访问会均摊带宽。
- 默认情况下,多个设备会均摊带宽,家庭路由器管理界面可以对指定设备限制网速。
二、复盘
2.1 用户直观感受
造成的后果是:首页加载很慢、背景图加载很慢、首页上按钮点击多点几次才生效、背景音乐卡顿。
2.2 查看监控
2.2.1 埋点统计
通过预先设置的埋点可以看到,在12点34分上线后的15分钟之内访问次数有216次:
当然这只是一个时间段内的访问次数,直接影响服务器的还是看下面的同时连接数。
2.2.2 查看ECS监控
刚上线后的几分钟最高同时连接数160个(服务器在单位时间内维持的TCP/UDP并发连接总数):
5Mbps服务器的带宽直接拉满:
2.3 计算和分析
2.3.1 要注意,多少兆带宽是指兆比特,而多少兆文件是指兆字节!!
带宽单位Mbps中的b是bit(比特)。我们平时说的多少兆带宽单位是Mbps,而不是MB/s。而文件大小都是用 bype 为单位
因为
所以 5Mbps 其实是:
也就是说1MB大小的文件,就算一个设备独享5Mbps的带宽也得下载 1.6s。
2.3.2 我的h5首屏要加载5.3MB静态资源
F12打开控制台,禁用缓存后重新加载h5可以看到 5.3MB 静态资源传输过来,一共 5.4MB 大小 (浏览器自动解压后)。
结合上文的ECS监控也可以看到5Mbps的带宽这段时间内被用满了。所以毋庸置疑,一定是资源太大、带宽太小导致的。
三、解决
下面记录一下我们平时背的八股文的实操过程。
(大部分是我上线之前就做了的,只是服务带宽没有提高,最重要的是我音频忘记压缩了,虽然音视频会自动分段加载,一次加载5MB左右,但是和总计有17.6MB...)
3.1 提高ECS带宽限制,包年付费改为按流量计费
最直接最快最有效的方案当然是提高带宽限制:
这种h5活动页的服务器只是短时间暴增流量,所以适合按流量计费。我设置的是100Mbps(2核4G的规格最高配100Mbps的带宽),但没有具体的数据支撑。这里我稍微估算一下:
5.3MB的资源,在100人同时访问的情况下,想让每个人都能在1s内加载完成,就需要:
这么算我设置100Mbps也远远不够。但考虑到活动也就刚刚发布的时候人很多,后面同时连接数也就不到50,而且每个连接加载的很多都是几十几百KB的其他小资源,所以先这样。
!!! 注意!!! 对于我这种场景其实应该设置包年包月下的临时带宽升级,因为按量计费每小时都会有 0.4元的服务配置费用,一年下来得 3504 元。当时太急了没看到这个功能,这一步是我踩了坑了。
后面我会尝试通过自定义镜像的方式来备份文件数据(保存快照),更换系统后使用该镜像替换系统。成功的话我再记录下来。
3.2 图片、音频等静态资源压缩
3.2.1 图片压缩
推荐 iloveimg。
这个h5很多图片,设计师给到我的每张都是2MB以上,需要在保证合适清晰度的情况下尽可能将空间压缩到最小。最后所有2MB的图片都控制在200KB左右:
3.2.2 音频压缩
这个h5 有个 17.6MB 的背景音乐。这个是导致带宽不足的罪魁祸首,必须压缩:www.youcompress.com/zh-cn/mp3/
背景音乐对音质要求不是很高,放心压缩就好,况且经过这么大幅度的压缩,我真没听出来有什么变化。
3.2.3 代码压缩
代码本身不占太多体积,使用vite、webpack等构建工具在生产环境下会自动进行代码压缩,主要有:
| 类型 | 压缩方式 |
|---|---|
| JavaScript | 移除空格、注释、简化变量名(minify) |
| CSS | 移除冗余样式、空格、注释 |
| HTML(可选) | 需要插件支持,例如 html-webpack-plugin 配合 minify 选项 |
相对图片、音频等大体积的静态资源,代码压缩效果相对不会很明显,这里不做详细介绍。
3.3 开启gzip压缩
很多前端面试八股文中都会提到gzip压缩,但其实前端开发不需要做任何事情!!
3.3.1 gzip是什么?
gzip是一种压缩算法,当然还有其他算法,而nginx默认支持gzip,兼容性也是最好的:
| 特性 | Gzip | Brotli (br) | Zstandard (zstd) | Deflate |
|---|---|---|---|---|
| 📦 压缩率(文件变小程度) | 中 | 高(优于 gzip) | 高(≈ Brotli) | 中 |
| 🚀 解压速度 | 快 | 中等 | 快(优于 gzip) | 快 |
| ⚙️ 压缩速度 | 快 | 慢(尤其压缩等级较高) | 非常快 | 快 |
| 🧠 CPU 占用(压缩阶段) | 低 | 中高 | 中 | 低 |
| 🌐 浏览器支持度(客户端) | ✅ 全部支持 | ✅ 现代浏览器支持 | 🔶 Chrome/Firefox ≥118 | ✅ 老浏览器也支持 |
| 🔧 Nginx 原生支持 | ✅ 支持(默认) | ❌ 不支持 | ❌ 不支持 | ✅ 支持(内置于 gzip 模块) |
| 🔌 Nginx 扩展支持方式 | 无需额外模块 | ✅ 静态 .br 文件配合 gzip_static | ✅ 第三方模块,如 ngx_zstd | 内置 |
| 📁 静态压缩文件支持(推荐) | ✅ .gz | ✅ .br | ✅ .zst(需自定义配置) | ❌ 很少使用 |
| 🧰 构建工具支持(Vite/Webpack) | ✅ 普遍支持 | ✅ 插件良好 | ✅ 插件可用 | 几乎没人用 |
| 🏗️ 适合动态页面压缩 | ✅ 性能佳 | ⚠️ CPU 开销高 | ⚠️ Nginx需模块,不推荐动态用 | ✅ 简单页面可用 |
| 📦 Nginx 安装难度 | 简单 | 中等(需配置静态) | 高(需打补丁或 OpenResty) | 已集成 |
3.3.2 过程
gzip压缩的过程是下面这样的。
- 前端请求时,浏览器会自动带上请求头
accept-encoding,以告诉服务器哪些压缩算法:
- 服务器端(如 Nginx)检测到这个头,就会:
-
检查
nginx.conf中gzip on是否启用; -
检查资源 MIME 类型是否在
gzip_types中; -
检查是否符合
gzip_proxied策略;
都符合就会对响应内容进行gzip压缩,并在响应头加上Content-Encoding: gzip。
注意:以上过程是纯后端对每个静态资源的请求的实时压缩,这样在高并发的场景下会大量消耗
CPU资源,所以常常需要前端打包时就接触构建工具插件来构建一个.gz的文件交给后端,后端在nginx中开启gzip_static选项,这样nginx会优先查找是否有现成的.gz文件。
gzip_static模块需要nginx另外安装,默认的nginx没有安装这个模块。
3.3.3 配置前后数据对比
注意,仔细分析每一条网络请求最好使用浏览器的隐私窗口打开,这可以排除谷歌扩展工具等发出的和网页无关的请求。
3.3.3.1 开启gzip压缩之前
F12 打开网路面板中的“大请求行”可以看到每个资源的原始空间大小(下)以及传输的空间大小(上)。正常来说传输的空间大小比资源原始的空间大小多200B~300B左右HTTP头部占用的空间:
3.3.3.2 开启gzip压缩之后
以下是nginx对gzip的默认配置:
gzip on;
# gzip_vary on;
# gzip_proxied any;
# gzip_comp_level 6;
# gzip_buffers 16 8k;
# gzip_http_version 1.1;
# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
默认只对 text/html; 的类型应用gzip压缩。gzip_type配置允许你对其他类型启用gzip压缩:
解开上面3-8行的注释后记得重启nginx再查看效果,其他配置这里不做赘述,可以查看官方文档。
从上图可以看到,开启gzip压缩之后,符合gzip_type配置的资源请求的响应头中会有Content-Encoding: gzip。这表示已经生效。再查看空间大小:
3.3.3.3 压缩前后大小对比,压缩率计算
考虑到 HTTP 请求头的空间占用开销,以下是严谨的压缩率计算公式:
对比表格如下:
| 文件名 | 原始大小(未压缩) | Gzip压缩前的传输大小 | Gzip压缩后的传输大小 | 压缩率 |
|---|---|---|---|---|
| %E9%80%89%E9%xxx.js | 354 B | 614 B | 494 B | 40.68% |
| home-BmvMRGK4.css | 525 B | 771 B | 572 B | 43.62% |
| navigation-CByOFk4A.js | 1.2 kB (1229 B) | 1.4 kB (1434 B) | 989 B | 54.67% |
| home-CPoM8DtW.js | 1.3 kB (1331 B) | 1.6 kB (1638 B) | 1.1 kB (1126 B) | 59.66% |
| wxshare.js | 3.2 kB (3277 B) | 3.5 kB (3584 B) | 1.6 kB (1638 B) | 92.10% |
| audio.js | 4.6 kB (4710 B) | 4.8 kB (4915 B) | 2.1 kB (2150 B) | 92.59% |
| index-CJAT1vev.css | 75.4 kB (77210 B) | 75.6 kB (77414 B) | 5.9 kB (6041 B) | 99.59% |
| index-BV-EnN-h.js | 87.4 kB (89498 B) | 87.6 kB (89698 B) | 35.1 kB (35942 B) | 98.30% |
可以看到,gzip的压缩确实能节省一定的带宽,尤其是相对于较大的文件,效果更为明显。
3.3.3.4 为什么不对图片和音频开启gzip?
这里要分清什么是无损压缩(可逆压缩)、什么是有损压缩(不可逆压缩)。
gzip 是无损压缩,压缩后可通过某种算法操作得到原本的数据(这个过程就是所谓的解压缩)。
而我在这之前已经对图片和音频进行了有损压缩,以牺牲图片质量和色彩信息为代价,过程是不可逆的,也就是我无法通过某种算法再得到压缩前的数据。
对一个已经经过有损压缩的文件二次做无损压缩,通常不会有任何效果、甚至体积反而会增大。
(参考)
3.4 预加载
这个h5每页都会有几张几百KB的图片(压缩之后),如果到那个页面再去加载图片有个缓慢加载的过程,弱网环境下体验不好。下面是几种预加载方案的实操。
3.4.1 给new Image()的实例赋src,进行js预加载
/**
* 预加载单张图片
* @param {string} src - 图片路径
* @returns {Promise} - 返回Promise,加载成功时resolve,失败时reject
*/
export const preloadImage = (src) => {
return new Promise((resolve, reject) => {
if (!src) {
reject(new Error('图片路径不能为空'));
return;
}
const img = new Image();
img.onload = () => {
resolve({
src: src,
status: 'loaded',
width: img.width,
height: img.height
});
};
img.onerror = () => {
reject(new Error(`图片加载失败: ${src}`));
};
img.src = src;
});
};
使用这种方式在当前页预加载下一页的图片后,再到下一页时就不会再次加载,你可以通过浏览器NetWork面板验证,但是要记得关掉Disable cache,否则它仍然会加载2次。
3.4.2 使用link标签的rel="preload"预加载图片
那么你可能会问第一页的图片如何预加载?答案是使用rel="preload"的link标签(附上MDN文档)。
在vue这样的spa框架中,直接在根目录的index.html的head标签中添加:
<!-- 预加载首页背景图 -->
<link rel="preload" href="/src/assets/P01-封面/背景3.png" as="image" type="image/png" />
<!-- 预加载首页其他关键图片 -->
<link rel="preload" href="/src/assets/P01-封面/你是.png" as="image" type="image/png" />
<link rel="preload" href="/src/assets/P01-封面/按钮.png" as="image" type="image/png" />
注意,当使用rel="preload"时,as属性必填,它指定了 <link> 正在加载的内容类型,这对于匹配请求、应用正确的内容安全策略和设置正确的 Accept 请求标头都是必要的(附上MDN的文档)。
而type属性告诉浏览器预加载资源的 MIME 类型,浏览器以此来确定是否支持该资源,如果不支持,则会忽略它,仅在支持时才会下载它。
通过Performance面板可以看到,通过link ref="preload"预加载的图片在DCL之前即可完成加载:
3.4.3 音效预加载
如果需要有交互音效,那一定得预加载,否则点击之后再加载就会反馈不同步。废话不多说,这个直接贴代码:
// 使用AudioContext优化点击音效
let audioContext;
let clickSoundBuffer = null;
let isAudioInitialized = false;
const CLICK_SOUND_PATH = './mixkit-arcade-game-jump-coin-216.wav';
// 初始化音频上下文和加载音效
function initAudio() {
try {
// 创建音频上下文
window.AudioContext = window.AudioContext || window.webkitAudioContext;
audioContext = new AudioContext();
// 加载点击音效
const request = new XMLHttpRequest();
request.open('GET', CLICK_SOUND_PATH, true);
request.responseType = 'arraybuffer';
request.onload = function() {
audioContext.decodeAudioData(request.response, function(buffer) {
clickSoundBuffer = buffer;
isAudioInitialized = true;
});
};
request.send();
} catch (e) {
console.error('Web Audio API不受支持,回退到传统方式');
// 回退到传统方式
useTraditionalAudio();
}
}
// 回退到传统Audio方式
let clickAudio;
function useTraditionalAudio() {
clickAudio = new Audio(CLICK_SOUND_PATH);
clickAudio.volume = 0.3;
isAudioInitialized = true;
}
// 播放点击音效和振动
function playClickSound() {
// 首次调用时初始化音频
if (!isAudioInitialized) {
initAudio();
// 由于初始化需要时间,第一次点击直接返回,只设置振动
if (navigator.vibrate) {
navigator.vibrate(50);
}
return;
}
// 使用低延迟的AudioContext播放
if (clickSoundBuffer && audioContext) {
// 创建音源
const source = audioContext.createBufferSource();
source.buffer = clickSoundBuffer;
// 创建音量控制
const gainNode = audioContext.createGain();
gainNode.gain.value = 0.3; // 音量设置
// 连接音源到音量节点再到输出
source.connect(gainNode);
gainNode.connect(audioContext.destination);
// 立即播放
source.start(0);
} else if (clickAudio) {
// 回退方案
clickAudio.currentTime = 0;
clickAudio.play();
}
// 添加手机振动
if (navigator.vibrate) {
navigator.vibrate(50); // 振动50毫秒
}
}
// 页面加载时预初始化音频
document.addEventListener('DOMContentLoaded', function() {
// 用户首次交互时初始化音频
const initOnFirstInteraction = function() {
initAudio();
// 移除事件监听器
document.removeEventListener('click', initOnFirstInteraction);
document.removeEventListener('touchstart', initOnFirstInteraction);
};
document.addEventListener('click', initOnFirstInteraction);
document.addEventListener('touchstart', initOnFirstInteraction);
});
这段代码使用 两种音频播放方案,用最低延迟的 Web Audio API 实现音效播放,并为不支持 Web Audio 的旧浏览器做了 降级处理,改个 CLICK_SOUND_PATH 就可以拿去直接用。
3.5 使用CDN将源站资源缓存到遍布全球的边缘节点
因为阿里云CDN要单独计费,得17块/月,而且我这场景太小了没必要上CDN,我这里就不实操了,简单介绍一下阿里云CDN。
阿里云内容分发网络CDN(Content Delivery Network)是建立并覆盖在承载网之上,由遍布全球的边缘节点服务器群组成的分布式网络。阿里云CDN能分担源站压力,避免网络拥塞,确保在不同区域、不同场景下加速网站内容的分发,提高资源访问速度。
阿里云CDN在全球拥有3200+节点。中国内地拥有2300+节点,覆盖31个省级区域,大量节点位于省会等一线城市;海外、中国香港、中国澳门和中国台湾拥有900+节点,覆盖70多个国家和地区。全网带宽输出能力达180 Tbps。
注意,他这里说全网带宽输出能力达180Tbps,这个带宽不是给你一个人用的,是全网共享的。
阿里云CDN单节点存储容量达40 TB ~ 1.5 PB,带宽负载达到40 Gbps ~ 200 Gbps ,具备180 Tbps带宽储备能力。
注意,他这里说的又是“单节点”,也不是给一个人用的,是单节点服务器的所有用户共享的。
总之,具体下行速度得实操才知道,但肯定的几个优点是,能让全球各地的用户更快得获取静态资源,而且这些不走源站的带宽,可以给源站减轻很多并发压力。
四、总结
以上介绍了本次的分析和实操过程,总结一下就是:
- 首先务必对静态资源手动压缩,千万不要让用户加载
1MB甚至10MB的单个静态资源; ToC的页面一定要评估一下流量,尤其是活动页会在某个时间点迎来一次暴增流量,上线之前要做压力测试,上线时刻做好升级服务器配置的准备;- 对于大部分格式的文件可以开启
gzip压缩。经过实测,对较大的文件(几十KB)gzip的压缩率能到90%以上,最主要的是它兼容性好,配置简单。 - 代码中可以对静态资源预加载,并使用loading、骨架屏等提高用户体验;
- 如果用户分布较广,用户量很大,服务器带宽不够,那么建议使用CDN将源站资源缓存到遍布全球的边缘节点;
此外,我之前的一篇《首屏加载优化实测——按需加载、分包》中已经介绍了按需加载和分包的方式来优化组件库的静态资源,但本次h5的静态资源优化实操不涉及这两个方面,就不重复介绍。