
哈喽大家好,今天继续为大家分享最新的面试题,今天我们一起来看看计算机网络相关的内容。
一、浏览器机制
1、浏览器渲染流程(解析 HTML→生成 DOM 树→CSSOM 树→渲染树→布局→绘制→合成)
我们在日常的开发中,无非就两种架构方式:B/S和C/S架构,而对于一个B/S架构的系统来说,我们的页面主要是通过浏览器来渲染的。那浏览器具体是怎样选渲染页面的呢?主要分为以下几个阶段。
先来看一张图片:

接下来听我解释:
浏览器渲染页面主要分为两个阶段:
第一阶段:渲染前资源加载:
https://www.example.com)后,浏览器先解析 URL 协议(HTTP/HTTPS)、域名(www.example.com)、端口(默认 80/443)和路径;192.168.1.1),确定资源所在的服务器位置。第二阶段:渲染阶段:拿到数据之后展示在页面上
当我们的浏览器从服务器获取到 HTML、CSS 等核心资源后,会由 渲染线程 执行以下流程,最终将资源转化为用户可见的页面。这一阶段正是你提到的 “解析 HTML→生成 DOM 树→CSSOM 树→渲染树→布局→绘制→合成”,下面补充每个步骤的详细逻辑:
补充,这里提一个概念:服务端渲染。如果对它的原理感兴趣的小伙伴可以自己查阅资料了解一下。
<div>、class="box"、“Hello”);(补充:为什么HTML5提出了语义化的标签?)<div> 对应一个 HTMLDivElement 节点,文本对应 Text 节点);document,子节点为 <html>,再下为 <head> 和 <body>,以此类推)。(补充:为什么vue3提出了虚拟DOM的概念?有哪些好处?)<script src="app.js"></script>),默认会 暂停 HTML 解析(因为 JS 可能通过 document.write() 等操作修改 DOM,浏览器需先执行 JS,再继续解析);async 或 defer 属性让 JS 异步执行,避免阻塞 HTML 解析(async 加载完立即执行,defer 等待 HTML 解析完再执行)。<style> 标签读取,内联 CSS 从元素的 style 属性读取);.box、color、red)和 “语法分析”(验证 CSS 语法合法性,忽略无效样式);<body> 默认有 margin: 8px)。<head>、display: none 的元素等)。
<script>、<meta>、display: none 的元素);.box 的 color: red 会覆盖元素默认的文本颜色);<div class="box">Hello</div>,CSSOM 中 .box { color: red; font-size: 16px; },则渲染树中该节点会绑定 “文本内容 Hello + 红色 16px 字体” 的样式。
精确位置(x/y 坐标) 和 尺寸(width/height),生成 “布局树”(Layout Tree,也叫 “盒模型树”)。
<div>):计算其在父容器中的水平位置(margin-left/padding-left 等)、垂直位置(受前一个元素高度影响)、宽度(默认继承父容器宽度)、高度(由内容高度或 height 属性决定);<span>):按文本流排列,计算其在一行中的位置和尺寸;viewport 尺寸);width、删除节点),会触发 重排(Reflow),即重新执行布局阶段,这会消耗较多性能(避免频繁触发重排,如批量修改样式、使用 transform 代替 top/left)。像素缓冲区(即 “图层”,Layer)中,生成 “像素图”。
color、background-color,不影响位置和尺寸),会触发 重绘(Repaint),性能消耗比重排小,但仍需优化(如避免频繁修改颜色)。屏幕帧缓冲区,由显示器显示出来。
transform: translateZ(0)、opacity < 1、有 CSS 动画的元素),目的是让这些元素的绘制 / 合成独立于其他元素,减少性能消耗;z-index 高的图层在上方)合并成一个 “最终帧”;transform 做动画),只需重新合成该图层,无需触发重排或重绘,这是 CSS 动画性能优化的核心原理(避免使用 top/left 做动画,改用 transform)。2、本地存储(Cookie/LocalStorage/SessionStorage/indexedDB 对比);
具体内容请看下面这张表格:
特性 | Cookie | LocalStorage | SessionStorage | IndexedDB |
|---|---|---|---|---|
存储大小 | 4KB 左右 | 5-10MB | 5-10MB | 较大(一般无硬性限制) |
有效期 | 可设置过期时间 | 永久存储,除非手动清除 | 仅在当前会话有效,关闭标签页 / 浏览器后清除 | 永久存储,除非手动清除 |
存储位置 | 客户端和服务器端(每次请求自动携带) | 仅客户端 | 仅客户端 | 仅客户端 |
数据类型 | 仅字符串 | 仅字符串(需手动序列化对象) | 仅字符串(需手动序列化对象) | 结构化数据(支持多种数据类型) |
访问权限 | 遵循同源策略,可设置 domain 和 path 限制 | 严格遵循同源策略 | 不仅遵循同源策略,还限制在同一标签页 | 严格遵循同源策略 |
存储方式 | 键值对 | 键值对 | 键值对 | 事务型数据库,支持索引 |
API 复杂度 | 较简单(document.cookie) | 简单(setItem, getItem 等) | 简单(同 LocalStorage) | 较复杂(异步操作,事务处理) |
适用场景 | 身份认证、记住登录状态等小型数据 | 长期存储用户偏好设置、离线数据等 | 临时存储表单数据、页面间传递数据 | 存储大量结构化数据、离线应用数据 |
性能 | 每次请求都会携带,可能影响性能 | 同步操作,大量数据操作可能阻塞主线程 | 同步操作,仅当前会话有效 | 异步操作,适合大量数据处理,性能好 |
看完了上面的内容,那这里举个例子:我们的token一般存在哪?为什么要这么存?有什么风险?如何规避这种风险?
二、网络协议
1、HTTP 请求结构(请求行 / 头 / 体)
在回答这个问题之前,我们先来看一个完整的HTTP请求:
POST /api/users HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
Content-Type: application/json
Content-Length: 36
{"name": "Alice", "age": 30, "email": "a@x.com"}上面的请求中,第一行就是请求行,主要包含三个要素:请求方法,请求的URL地址和HTTP协议的版本。
常见的请求方法有四种,表示对资源的操作意图,有 GET(获取资源)、POST(提交数据)、PUT(更新资源)、DELETE(删除资源)等。那请求 的URL就指定请求的资源路径(相对路径或绝对路径),最后还有一个HTTP协议的版本,如 HTTP/1.1、HTTP/2 等。
接下来是请求头:它是一系列的键值对,用于描述请求的元信息(如客户端类型、接受的数据格式、认证信息等),每行一个键值对,格式为 Key: Value。
常见请求头示例:
Host: www.example.com:指定服务器域名和端口User-Agent: Mozilla/5.0 (Windows NT 10.0; ...):客户端浏览器 / 设备信息Accept: application/json:客户端可接受的数据格式Content-Type: application/x-www-form-urlencoded:请求体的数据格式Authorization: Bearer <token>:身份认证信息Cookie: name=value; sessionId=xxx:客户端存储的 Cookie 数据请求头结束之后就是我们的请求体:
这一部分并不是每个HTTP请求中都有,只有在需要向服务器提交数据时使用(如 POST、PUT 方法)它来携带具体的数据内容。
请求体的格式由 Content-Type 头指定,常见类型:
application/x-www-form-urlencoded:表单数据(如 name=John&age=20)multipart/form-data:文件上传或复杂表单数据application/json:JSON 格式数据(如 {"name":"John","age":20})text/plain:纯文本数据2、响应状态码(2xx/3xx/4xx/5xx 高频码含义,如 304 缓存、401 权限、403 禁止)
既然是浏览器和服务器之间的通信,而且中间要经过网络层,那我们就很难确保请求和响应一切正常,出现了问题我们怎么排查?为什么前端传递了参数,页面上却没有显示后端返回的内容?到底是前端的问题还是后端的问题还是网络的问题,那接下来这些状态码将会告诉我们问题究竟在哪里?
HTTP的响应状态码主要分为五类,分别如下:
表示服务器已接收请求,正在处理中(较少见)。
表示请求已被服务器成功接收、理解并处理。
表示需要客户端进一步操作才能完成请求(通常是跳转)。
If-Modified-Since 等使用,减少带宽消耗)。表示请求存在错误,服务器无法处理。
表示服务器处理请求时发生内部错误。
Retry-After 头提示重试时间。3、HTTP 1.1/2.0/3.0 差异、HTTPS 工作原理(TLS 握手过程 + 加密方式);
这里由于篇幅原因,给大家整理了一张表格:
特性 | HTTP 1.1 | HTTP 2.0 | HTTP 3.0 |
|---|---|---|---|
发布时间 | 1999 年 | 2015 年 | 2022 年(标准化) |
底层协议 | TCP | TCP | QUIC(基于 UDP) |
连接方式 | 单连接,串行请求(需排队) | 单连接,多路复用(并行请求) | 单连接,多路复用(更高效) |
头部压缩 | 无(头部重复传输,冗余大) | HPACK 算法压缩 | QPACK 算法压缩(改进版) |
二进制协议 | 文本协议(易读但效率低) | 二进制协议(分帧传输,效率高) | 二进制协议 |
服务器推送 | 不支持 | 支持(主动推送关联资源) | 支持(更灵活的推送机制) |
队头阻塞问题 | 严重(一个请求阻塞后续所有请求) | 缓解(分帧避免连接级阻塞) | 解决(UDP 无队头阻塞特性) |
TLS 依赖 | 可选(HTTP 明文,HTTPS 加密) | 推荐但可选 | 强制加密(基于 TLS 1.3) |
典型性能场景 | 简单页面,请求量少 | 复杂页面,多资源并行加载 | 高延迟网络(如移动网络)、大并发 |
三、性能优化
1、浏览器缓存(强缓存 / 协商缓存机制 + 字段)
运行的好端端的系统突然出了bug,于是你去找到了技术大牛问怎么回事儿?他告诉你:要不清下缓存呢?
这里的缓存是什么意思,为什么清除了缓存就有可能让我们的系统重新跑起来?缓存中到底存了啥?
浏览器缓存其实是为了提升网页加载速度、减少服务器压力而采取的一种重要机制,主要分为强缓存和协商缓存两类,两者配合使用可最大化缓存效率。
接下来,一起来看看它俩的区别:
强缓存由浏览器自主判断是否使用本地缓存,无需向服务器发送请求。若缓存有效,则直接使用本地资源(状态码 200 OK (from cache) 或 200 OK (from memory cache))。
Expires: Wed, 26 Sep 2025 12:00:00 GMTCache-Control 替代。Expires)常用值:
示例:Cache-Control: max-age=86400, public 表示资源可被任何节点缓存,有效期 1 天。max-age=<seconds>:缓存有效时长(相对时间,如 max-age=3600 表示 1 小时)。public:允许任何节点(如 CDN、代理服务器)缓存。private:仅客户端可缓存(默认值,代理服务器不可缓存)。no-store:禁止任何缓存(每次都请求服务器,不存本地)。no-cache:不使用强缓存,需进入协商缓存(每次请求服务器验证)。当强缓存失效(过期或配置 no-cache)时,浏览器会携带缓存标识向服务器请求,由服务器判断是否使用缓存:
304 Not Modified,浏览器使用本地缓存。200 OK 和新资源,浏览器更新缓存。Last-Modified: Wed, 20 Sep 2025 10:00:00 GMT:服务器告知资源最后修改时间。If-Modified-Since: Wed, 20 Sep 2025 10:00:00 GMT:浏览器携带上次的 Last-Modified 值,询问服务器 “该时间后资源是否修改过?”。304;若已修改,返回新资源和新 Last-Modified。ETag: "abc123":服务器对资源内容生成唯一哈希值(如 MD5),内容变则哈希变。If-None-Match: "abc123":浏览器携带上次的 ETag 值,询问服务器 “当前资源哈希是否与该值一致?”。304;若不一致(已修改),返回新资源和新 ETag。Cache-Control 或 Expires 未过期,直接使用本地缓存(不发请求)。no-cache,进入协商缓存。If-Modified-Since(时间戳)或 If-None-Match(哈希)请求服务器。304(用缓存)或 200(更新缓存)。max-age(如 1 年),配合文件名哈希(如 app.abc123.js)实现更新(哈希变则视为新资源)。
ETag 确保内容准确性。
Cache-Control: no-store。
思考一下:我们怎么合理的配置缓存?哪些数据适合缓存,哪些数据不适合缓存?
2、首屏加载优化(压缩资源、懒加载、CDN 等)。
有一天,你写代码写着写着不想写了,你打算打开一个视频网站摸一会儿🐟,当你在浏览器输入url后按下了回车,页面在加载,过了三分钟,怎么还在加载?于是你默默的吐槽:谁tm写的垃圾玩意儿,一个首页加载了五分钟!
从刚刚的介绍中你应该也能感受到,首屏加载速度很影响用户的体验啊!
那遇到刚刚那种网站,我们不得优化优化,不要哪一天让用户真正骂到我们自己头上来了。。。。那怎么优化呢?我们可以从 “减少资源体积、优化加载顺序、提升加载效率”这些角度入手,也可以从网络传输、资源处理、渲染机制等多维度切入。
首屏加载的核心瓶颈往往是 “资源过多、体积过大”,需从源头压缩请求成本。
Terser 移除注释、空格、未使用代码(Tree-Shaking),避免冗余逻辑(如重复工具函数)。CSSNano 压缩代码,配合 PurgeCSS 移除未使用样式(如引入 Bootstrap 但仅用 10% 样式)。html-minifier 压缩标签、移除空格和注释(尤其静态页面)。Squoosh(在线工具)或 Sharp(Node 库)压缩,保留视觉无损(如 JPG 压缩质量 80%,PNG 用 optipng 优化)。font-spider 提取子集。font-display: swap(先显示系统字体,字体加载完成后替换,避免文字闪烁)。首屏加载需遵循 “先关键、后非关键” 的原则,确保首屏核心内容(如标题、导航、主要文案)优先加载。
<head> 中(避免 “外部 CSS 阻塞渲染”,减少 1 次网络请求),非首屏 CSS(如 footer 样式)用 media="print" 标记为 “非关键”,延迟加载。@import 引入 CSS(会导致 CSS 串行加载,比 <link> 并行加载慢)。async:JS 加载完成后立即执行(顺序不保证,适合独立脚本如统计代码)。defer:JS 加载完成后,等待 DOM 解析完成再执行(顺序保证,适合依赖 DOM 的脚本)。async 或 defer 避免阻塞 DOM 解析:document.createElement('script')),或在 window.onload 后加载。<template> 或动态插入,减少初始 DOM 解析量。Cache-Control: max-age=31536000,1 年),配合 “文件名哈希”(如 app.abc123.js)—— 资源更新时哈希变化,自动触发重新加载。Cache-Control: no-cache + ETag),确保每次请求验证是否更新,避免旧 HTML 导致资源引用错误。brotli on; 模块,Apache 需启用 mod_brotli。dns-prefetch:<link rel="dns-prefetch" href="//cdn.example.com">,提前解析 DNS(减少 20-120ms 延迟)。preconnect 提前建立 TCP + TLS 连接:<link rel="preconnect" href="//api.example.com">,避免请求时的握手延迟。即使资源加载完成,若渲染过程存在阻塞(如 JS 执行过长、重排重绘),仍会导致首屏显示延迟。
requestIdleCallback),在浏览器空闲时执行,不阻塞渲染。DOMContentLoaded 或 load 事件中执行大量同步代码,优先渲染 UI,再处理非关键逻辑。div:nth-child(2).class),浏览器匹配选择器是 “从右到左”,简单选择器(如 .class、#id)匹配更快,减少渲染耗时。class 切换样式,而非多次 element.style 操作),避免频繁触发重排(如改变 width、height、top 等属性)。transform 和 opacity 实现(仅触发复合层,不触发重排重绘),避免用 left、margin 等属性。loading="lazy" 属性(浏览器原生支持),或 Intersection Observer 监听元素是否进入视口,再加载资源(避免首屏加载下方图片)。React.lazy(() => import('./Component'))),非首屏组件(如弹窗、详情页)仅在需要时加载。<link rel="preload" href="critical.js" as="script"> 强制提前加载,避免 “关键资源加载延迟”。<link rel="preconnect" href="//third-party.com"> 提前建立连接,减少后续请求的握手延迟。优化后需通过工具量化效果,确保首屏加载速度达标:
扩展:
1、介绍一下 TCP 握手过程
由于篇幅原因,这里请大家看下面的表格:
步骤 | 发送方 | 接收方 | 数据包内容(核心字段) | 目的 |
|---|---|---|---|---|
1 | 客户端 | 服务器 | SYN = 1,Seq = x(初始序列号) | 客户端向服务器 “请求建立连接”:1. SYN=1 表示这是 “同步请求包”(发起连接);2. Seq=x 是客户端生成的随机初始序列号(后续客户端发送数据会从 x+1 开始)。 |
2 | 服务器 | 客户端 | SYN = 1,ACK = x+1,Seq = y | 服务器 “响应连接请求,并同步自身信息”:1. SYN=1 表示服务器也向客户端发起同步(确认双方都要建立连接);2. ACK=x+1 表示 “已收到客户端的 Seq=x,下一次期待接收 x+1”(验证客户端接收能力);3. Seq=y 是服务器生成的随机初始序列号(后续服务器发送数据从 y+1 开始)。 |
3 | 客户端 | 服务器 | ACK = y+1 | 客户端 “确认收到服务器响应,连接正式建立”:1. ACK=y+1 表示 “已收到服务器的 Seq=y,下一次期待接收 y+1”(验证服务器接收能力);2. 此时双方都确认对方能收发数据,TCP 连接建立完成,后续可进入 TLS 握手阶段。 |
为什么需要三次,而不是两次呢?

因为两次握手无法验证 “服务器的接收能力”:若客户端发送的 SYN 包超时后到达服务器,服务器回复 SYN+ACK 但客户端已失效,服务器会一直等待客户端的 ACK,导致资源浪费(“半连接队列” 堆积)。
三次握手通过 “客户端最后一次 ACK”,确保服务器知道 “客户端能收到自己的响应”,避免无效连接,同时完成双向序列号协商,为后续可靠传输打下基础。
紧接着,我们来看看四次挥手的详细过程
步骤 | 发送方 | 接收方 | 数据包内容(核心字段) | 目的 |
|---|---|---|---|---|
1 | 客户端 | 服务器 | FIN = 1,ACK = z | 客户端 “请求关闭自身发送数据的方向”(半关闭):1. FIN=1 表示 “终结连接请求”(客户端不再向服务器发数据);2. ACK=z 是对服务器之前发送数据的确认(确保之前的数据已接收完成);3. 此时客户端进入 FIN_WAIT_1 状态,等待服务器响应。 |
2 | 服务器 | 客户端 | ACK = w+1 | 服务器 “确认收到客户端的关闭请求”:1. ACK=w+1 表示 “已收到客户端的 FIN 包,下一次期待接收 w+1”(w 是客户端 FIN 包的序列号);2. 此时服务器进入 CLOSE_WAIT 状态,客户端进入 FIN_WAIT_2 状态;3. 注意:服务器此时仍可向客户端发送未完成的数据(如剩余的响应内容)。 |
3 | 服务器 | 客户端 | FIN = 1,ACK = w+1,Seq = v | 服务器 “完成数据发送,请求关闭自身发送数据的方向”:1. FIN=1 表示 “服务器不再向客户端发数据”;2. ACK=w+1 再次确认客户端的 FIN 包(避免丢失);3. Seq=v 是服务器最后发送数据的序列号;4. 此时服务器进入 LAST_ACK 状态,等待客户端确认。 |
4 | 客户端 | 服务器 | ACK = v+1 | 客户端 “确认收到服务器的关闭请求,进入等待期”:1. ACK=v+1 表示 “已收到服务器的 FIN 包,下一次期待接收 v+1”;2. 客户端不立即关闭,而是进入 TIME_WAIT 状态(等待 2*MSL,MSL 是报文段最大生命周期,通常为 1-2 分钟);3. 服务器收到 ACK 后,立即关闭连接(进入 CLOSED 状态);4. 客户端等待 TIME_WAIT 结束后,确认服务器已关闭,再关闭连接(避免服务器未收到 ACK 而重发 FIN 包)。 |
那为什么断开连接需要四次挥手呢?而不是三次?
好啦 ,本期文章就到这里,我相信很少有人会读到最后,感谢大家的阅读,我们下期再见啦~
下一期文章我们会推送vue相关的八股文和场景题,节前预计会推送两期,一期vue,一期react,内容目前已经整理了一大半了,可谓是干货满满呀!详细内容大家敬请期待哦~