普通视图
电影《穿普拉达的女王2》内地票房破7000万
高通前高管将加入英特尔,领导客户端计算与物理AI事业部
中国移动将推出AI-eSIM产品
广汽丰田4月销量54116台
苹果考虑让英特尔、三星代工设备处理器
白宫拟在AI模型发布前实施审查
听歌时间到,12 张你可能错过的 2025 华语乐坛好专辑
今天全国铁路预计发送旅客2300万人次
去哪儿旅行:五一“宝藏小城串游”预订量增121%
英派药业:拟全球发售4197.70万股H股,发售价不超21.75港元
4月份中国大宗商品价格指数为132.1点 同比上涨20.2%
百威亚太:一季度收入14.93亿美元 同比下降0.7%
港股汽车股持续走低,赛力斯跌超5%
剂泰科技寻求通过香港IPO募资21.1亿港元
马斯克就推特持股披露诉讼与美证监会达成和解
抖音集团副总裁:网传所谓红果短剧收费的消息不实
TCP/IP 与前端性能:从数据包到首次渲染的底层逻辑
TCP/IP 与前端性能:从数据包到首次渲染的底层逻辑
这是“从 URL 到页面展示”系列第三篇。前面两篇我们聊完了浏览器导航和 DNS 与传输层细节,今天我们钻进 TCP/IP 协议栈的核心,看看一个数据包究竟怎么跑完全程,以及它为什么直接影响前端最关心的性能指标 FP(首次渲染时间)。
一、前端性能的起点:FP 与 TTFB
面试官问“怎么优化页面加载速度”,你可以先说两个关键时间点:
- TTFB(Time To First Byte):从发起请求到收到服务器第一个字节的耗时 = DNS 解析 + TCP/TLS 连接 + 服务器处理 + 响应传输的第一个字节到达。
- FP(First Paint):从页面加载到浏览器首次绘制出像素的耗时 = TTFB + HTML 解析 + CSSOM 构建 + 渲染树构建 + 布局 + 首次绘制。
从这个公式可以看出:TTFB 里有一大块时间花在网络传输上,而网络传输的根基就是 TCP/IP。前端做性能优化,不能只盯着 JS 和 CSS,还得懂底层。
二、数据包的旅程:互联网的“快递系统”
互联网本质是一套理念和协议组成的体系架构,数据不是一整块丢进网线的,而是拆成一个一个数据包传输。
为什么拆包?
- 单个文件可能几十 MB,一次性发送会长时间占用整条链路,其它请求就得排队。
- 拆成小包后,可以利用带宽并发传输,提升传输效率和容错率。某个包丢了,只需要重发这一个,不用重发全部。
这些数据包最终会变成二进制数据帧,在物理介质上流动。
三、IP 层:只负责“送到”,不负责“送到位”
IP(Internet Protocol)是网络层的协议,职责非常单纯:根据 IP 地址,把数据包从源主机送到目标主机。
它做的是“尽力而为”的服务:
- 可能丢包
- 可能出错
- 可能不按顺序到达
- 不提供任何纠正机制
所以 IP 本身是一个“不可靠”的协议。前端请求的 HTML 文件结构严密,一个字节错位都可能导致渲染异常,怎么办?
答案就在传输层。
四、UDP vs TCP:两种“快递模式”
传输层运行在 IP 之上,负责将数据包交付到目标主机上的具体应用(通过端口号)。主要有两位选手:
UDP(User Datagram Protocol):只管快
- 不建立连接,直接发包。
- 不保证顺序,不重传丢失的包。
- 头部开销小,速度快。
适用场景:对实时性要求极高、能容忍少量数据丢失的音视频直播、视频通话、在线游戏。
TCP(Transmission Control Protocol):保证到位
对于 HTML、CSS、JS、图片这类 Web 资源,哪怕一个包出错都可能导致页面渲染异常。TCP 专门解决两个核心问题:
| 问题 | TCP 的解法 |
|---|---|
| 数据包在传输过程中丢失 | 超时重传机制 — 每发一个包,启动一个计时器,过期未收到确认就重发 |
| 数据包到达接收端顺序错乱 | 序号机制 — 每个包都带有序号,接收端按序号重新组装 |
因为要保证可靠性,TCP 比 UDP 慢 —— 但这恰恰是 Web 页面需要的可靠传输。
五、三次握手:建立可靠连接
在真正发送 HTTP 请求之前,TCP 需要通过三次握手建立连接。核心目的:同步初始序号,验证双方收发能力。
简化过程:
-
客户端 → 服务器:
SYN,带上初始序号J
“我想和你建立连接,我发送的包从 J 开始编号,你听得到吗?” -
服务器 → 客户端:
SYN + ACK,确认号J+1,同时带上自己的初始序号K
“听到了,你下一个从 J+1 开始发。我也想和你建立连接,我的包从 K 开始编号,你听得到吗?” -
客户端 → 服务器:
ACK,确认号K+1
“听到了,你下一个从 K+1 开始发。咱们可以正式开始传数据了。”
高频追问:为什么是三次,不是两次或四次?
- 两次不够:服务器无法确认客户端能收到自己的消息(无法确认客户端有接收能力)。
- 四次没必要:第二次握手时,服务器把 “响应客户端的 SYN” 和 “发出自己的 SYN” 合并成一条消息,效率最大化。
原则是:每一方的发送和接收能力都需要两次验证,但因为服务器把两个动作合并了,总共只需三次。
过程图
六、四次挥手:优雅地断开连接
数据传输完毕(比如 HTML 下载完成、图片加载结束),需要断开 TCP 连接释放资源。这个过程需要四次挥手:
-
A → B:
FIN,带上序号M
“我没有数据要发了,想断开连接。” -
B → A:
ACK,确认号M+1
“知道你没数据了,但我可能还有数据没发完,你再等等。” -
B → A:
FIN,带上序号N
“我的数据也发完了,可以断开了。” -
A → B:
ACK,确认号N+1
“好的,我知道你也没数据了。再见。”
为什么比握手多一次?因为 TCP 是全双工的,双方都可以独立发送和接收数据。一端说“我发完了”,另一端可能还有数据要传,所以 FIN 和 ACK 不能合并,必须分开发送,正好四次。
过程图
![]()
七、回到前端:这些对性能优化意味着什么?
理解了 TCP,就能看懂很多性能优化的底层逻辑:
| 优化手段 | 背后的 TCP 原理 |
|---|---|
| 减少 HTTP 请求数(雪碧图、合并文件) | 每个 TCP 连接都有三次握手开销,请求数越少,握手成本越低 |
| 使用 HTTP/2 多路复用 | 单个 TCP 连接上并发传输多个请求和响应,避免重复握手 |
| 启用 TCP Fast Open | 在握手阶段就开始传数据,将握手和数据传输部分重叠,降低 TTFB |
| 使用 CDN | 缩短物理距离 → 减少 RTT(往返时延)→ 丢包概率降低 → 重传少 → 更快 |
| 资源预连接(preconnect) | 提前完成 DNS + TCP + TLS 握手,请求时直接使用已建立的连接 |
八、总结:一条链路串起知识体系
至此,我们串联起了整个前端性能链条的网络部分:
DNS 解析(IP 找到主机)
→ TCP 三次握手(建立可靠连接)
→ TLS 握手(加密安全)
→ HTTP 请求/响应(应用层数据)
→ TCP 四次挥手(断开连接)
每一个环节的耗时,都叠加进了 TTFB,进而影响 FP。下次面试问到性能优化,你完全可以从这个底层视角切入,展示你对网络协议栈的真懂,而不是只背“减少请求数”的表面答案。
代码写成一锅粥?3个设计模式让你的项目“起死回生”
你的组件里是不是全是
if-else?改一个地方,崩三个地方?新来的同事改完你的代码,你看着他,他看你,两人都沉默了。今天我们不背理论,直接用3个前端最常用的设计模式——单例、观察者、策略,把业务从“屎山”变成“积木”。学完你就能拍着胸脯说:“我的代码,谁都敢动。”
前言
设计模式不是“面试八股文”,而是前辈们踩过的坑总结成的“套路”。就像做饭有菜谱,写代码也有标准解法。今天我们把场景摆出来:弹窗多次打开、购物车更新通知到处写、表单校验if-else十几层……然后一个个用设计模式把它们治好。
一、单例模式:全局只有一个的“独生子”
场景:你写了个全局弹窗(Modal),用户点按钮就打开。结果用户连续点三次,页面上冒出三个弹窗叠在一起,像俄罗斯方块。
问题代码
function showModal() {
const div = document.createElement('div');
div.className = 'modal';
div.innerHTML = '我是弹窗';
document.body.appendChild(div);
}
// 点三次,三个弹窗
单例模式解决
确保无论调用多少次,只创建同一个实例。
class GlobalModal {
constructor() {
if (!GlobalModal.instance) {
this.element = null;
GlobalModal.instance = this;
}
return GlobalModal.instance;
}
show() {
if (!this.element) {
this.element = document.createElement('div');
this.element.className = 'modal';
this.element.innerHTML = '我是弹窗';
document.body.appendChild(this.element);
}
this.element.style.display = 'block';
}
hide() {
if (this.element) this.element.style.display = 'none';
}
}
const modal1 = new GlobalModal();
const modal2 = new GlobalModal();
console.log(modal1 === modal2); // true
真实项目更简单的写法:直接导出实例对象。
// modal.js
export const globalModal = {
element: null,
show() { /* ... */ },
hide() { /* ... */ }
};
应用:全局Store(Pinia/Vuex就是单例)、全局轮询管理器、WebSocket连接。
二、观察者模式:让不相干的组件“悄悄对话”
场景:用户点击“添加购物车”,需要同时做三件事:更新购物车角标、弹出“添加成功”提示、发送埋点数据。如果直接在购物车里调用其他模块的方法,代码会变成:
function addToCart(item) {
// 添加逻辑...
header.updateBadge(count);
toast.show('添加成功');
analytics.track('add_to_cart', item);
}
每加一个功能,addToCart就要改一次,耦合得像麻花。
观察者模式解决(事件总线)
// eventBus.js
class EventBus {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(cb => cb(data));
}
}
off(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
}
}
export const bus = new EventBus();
// 购物车模块
import { bus } from './eventBus';
function addToCart(item) {
// 添加逻辑...
bus.emit('cartUpdated', { count: newCount, item });
}
// 头部模块
import { bus } from './eventBus';
bus.on('cartUpdated', (data) => {
updateBadge(data.count);
});
// 埋点模块
bus.on('cartUpdated', (data) => {
analytics.track('add_to_cart', data.item);
});
现在,要加新功能只管bus.on,不用改购物车代码。Vue的emitter、React的useContext+useReducer其实都用了这个思想。
三、策略模式:消灭if-else毒瘤
场景:用户等级不同,商品折扣不同。你写了一个函数:
function getDiscount(level, price) {
if (level === 'normal') return price * 0.95;
else if (level === 'gold') return price * 0.9;
else if (level === 'platinum') return price * 0.8;
else return price;
}
这还好。但当你需要增加“钻石会员”、“企业会员”、“节日特惠”……函数越来越大,改一次心惊胆战。
策略模式解决:把算法抽成独立对象
const discountStrategies = {
normal: (price) => price * 0.95,
gold: (price) => price * 0.9,
platinum: (price) => price * 0.8,
};
function getDiscount(level, price) {
const strategy = discountStrategies[level];
return strategy ? strategy(price) : price;
}
新增会员等级,只需要加一个策略,不用改getDiscount。
更复杂的例子:表单验证
const validators = {
required: (val) => val && val.trim() !== '',
minLength: (val, len) => val.length >= len,
email: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),
};
function validateField(value, rules) {
for (let rule of rules) {
const [name, param] = rule.split(':');
const validator = validators[name];
if (validator && !validator(value, param)) {
return false;
}
}
return true;
}
// 使用
const isValid = validateField('abc@test.com', ['required', 'email']);
以后增加“手机号验证”,加一个mobile策略即可,完全符合开闭原则(对扩展开放,对修改封闭)。
四、组合实战:一个购物车结算页面
- 单例:全局唯一的购物车实例(存储商品列表)。
- 观察者:商品数量变化时,触发价格重算、优惠券校验、埋点。
- 策略:根据用户等级计算折扣;根据优惠券类型(满减、打折)计算优惠。
// 购物车单例
class Cart {
static instance = null;
static getInstance() {
if (!Cart.instance) Cart.instance = new Cart();
return Cart.instance;
}
items = [];
addItem(item) {
// 添加逻辑
bus.emit('cartChanged', this.items);
}
}
// 价格计算模块监听变化并应用折扣策略
bus.on('cartChanged', (items) => {
const total = items.reduce((sum, item) => sum + item.price * item.count, 0);
const discount = discountStrategies[user.level](total);
renderTotal(discount);
});
各模块独立,改折扣策略不影响购物车;加埋点不影响价格计算。
五、总结:模式是工具,不是教条
- 单例:保证全局唯一,适合共享资源。
- 观察者:解耦事件发布和订阅,适合跨组件通信。
- 策略:消除if-else,算法可互换,适合规则多变场景。
不要为了用模式而用模式。当你的代码出现重复、难维护、改一处动全身时,想想哪种模式能帮你“抽出来”。写代码就像搭积木,模式就是那些标准接口的积木块,让你搭得又快又稳。