1.性能指标
目前行业内主流的性能指标主要基于 Web Vitals 标准,分为核心指标和辅助指标:
|------------------|-------------------------------------------------------------|----------------------------------------------------------------------------------------|
| LCP | Largest Contentful Paint最大内容绘制 | 最重要的衡量页面的加载性能指标。指视口中最大的内容元素渲染完成的时间。优秀标准:≤ 2.5秒。 |
| FID/ INP | 首次输入延迟 First Input Delay/ Interaction to Next Paint | 衡量页面的交互性。指用户首次与页面交互(如点击按钮)到浏览器实际响应的时间 FID 优秀标准:≤ 100ms;INP 优秀标准:≤ 200ms INP 正在逐步替代 FID |
| CLS | Cumulative Layout Shift,累积布局偏移 | 衡量页面的视觉稳定性。指页面加载过程中元素意外移动的程度。优秀标准:≤ 0.1。 |
| FCP | First Contentful Paint 首次内容绘制 | 浏览器首次渲染出文本、图像或 SVG 等实际内容的时间。优秀标准:≤ 1.8秒。 |
| TTFB | Time To First Byte 首字节时间): | 请求发起到收到服务器第一个字节的时间,代表服务端响应和网络速度。优秀标准:< 600ms。 |
| TBT | Total Blocking Time 总阻塞时间 | 主线程被长任务(>50ms)阻塞的总时间。优秀标准:< 200ms。 |
获取这些性能指标
获取指标通常分为"合成监控(实验室数据)"和"真实用户监控(RUM)"两种方式:
浏览器原生 PerformanceObserver 可以监听并获取 FCP、LCP、CLS 等指标
Lighthouse:Chrome DevTools 内置的自动化审计工具,可以生成包含性能、可访问性等维度的详细报告,提供具体的优化建议。
PageSpeed Insights (PSI):Google 提供的免费服务,不仅提供 Lighthouse 的实验室模拟数据,还能结合 Chrome 用户体验报告(CrUX)提供真实用户的实测数据。
WebPageTest:用于在不同地理位置、不同设备和网络环境下进行多地点性能测试。
RUM(Real User Monitoring):通过嵌入 JS 脚本收集真实用户环境下的性能数据,反映真实的设备、网络条件和地理位置表现。
2.性能优化:
- 网络层面优化(更快、更小)
-
资源压缩:启用 Gzip 或 Brotli 压缩,通常能将资源体积减少 70% 以上。
-
CDN 加速:将静态资源部署到 CDN,缩短用户请求的物理距离。
-
缓存策略:合理配置强缓存(Cache-Control)和协商缓存(Etag/Last-Modified),减少重复请求。
-
减少请求:通过接口合并、雪碧图、小图片 Base64 内嵌等方式减少 HTTP 请求数。
- 资源加载优化(减少阻塞)
-
懒加载(Lazy Loading) :对非首屏的图片和组件使用
loading="lazy"或动态import()按需加载。 -
资源预加载 :使用
<link rel="preload">提前加载关键资源(如字体、首屏大图);使用preconnect提前建立第三方连接。 -
调整加载顺序 :CSS 放在
<head>中避免阻塞渲染,非关键 JS 使用defer或async异步加载。
- 构建打包优化(包体积更小)
-
Tree-Shaking:利用 ES6 模块语法,剔除未使用的死代码。
-
代码分割(Code Splitting):利用 Webpack/Vite 将代码拆分为多个 chunk,实现路由级或组件级按需加载。
-
按需引入:第三方 UI 库(如 Element/Antd)配置按需加载,避免全量打包。
- 代码逻辑与渲染优化(执行更快)
-
减少重排重绘 :避免频繁读取
offsetHeight等触发同步布局的属性;动画尽量使用transform和opacity开启 GPU 加速。 -
长任务优化 :将复杂计算交给 Web Worker,或使用
requestIdleCallback在浏览器空闲时执行非紧急任务。 -
长列表优化:使用虚拟滚动(Virtual List),仅渲染视口内可见的 DOM 节点。
-
内存管理:及时清理定时器、解绑事件监听,避免内存泄漏。
- 体验与感知优化(用户觉得更快)
-
骨架屏(Skeleton):在内容加载完成前展示占位 UI,降低用户的白屏焦虑。
-
交互反馈:高频操作(如搜索、滚动)使用防抖和节流;按钮点击后立即给予视觉反馈。
3.面试题
Q1:前端性能优化一般从哪些维度入手?
答:通常可以从六个维度入手:网络层面(CDN、压缩、缓存)、资源加载(懒加载、预加载)、构建打包(Tree-Shaking、代码分割)、代码逻辑(减少重排、Web Worker)、浏览器渲染(关键渲染路径、GPU加速)以及体验感知(骨架屏、防抖节流)。同时,优化必须遵循"指标量化 → 工具分析 → 方案实施 → 持续监控"的闭环逻辑。
Q2:什么是 Core Web Vitals(核心网页指标)?LCP、FID、CLS 分别代表什么?
答:Core Web Vitals 是 Google 定义的一组衡量用户体验的关键指标。LCP(最大内容绘制)衡量加载性能,优秀标准为 ≤2.5s;FID(首次输入延迟)衡量交互响应速度,优秀标准为 ≤100ms(现已逐步被 INP 替代);CLS(累积布局偏移)衡量视觉稳定性,优秀标准为 ≤0.1。
Q3:如何定位和解决前端性能瓶颈?
答:首先通过 Lighthouse 或 Chrome DevTools 的 Performance 面板生成报告,查看 FP/FCP/LCP 等核心指标和网络瀑布图,定位是大包加载慢还是主线程长任务阻塞。然后针对性解决:如果是加载慢,采用代码分割、图片压缩、CDN 等;如果是运行卡顿,采用虚拟列表、长任务时间切片、Web Worker 等方案。最后通过 RUM 监控持续观察优化效果。
Q4:什么是代码分割(Code Splitting)?Webpack 中如何配置?
答 :代码分割是将代码拆分成多个按需加载的 chunk,以减小首屏 JS 体积。在 Webpack 中,可以通过 SplitChunksPlugin 提取公共依赖;对于路由或组件,可以使用动态 import() 语法实现懒加载。在 Vite 中,原生支持动态导入,也可通过 build.rollupOptions.output.manualChunks 进行分包配置。
Q5:什么是 Tree-Shaking?写代码时有哪些注意事项能让它效果更好?
答 :Tree-Shaking 是一种剔除未使用代码(死代码)的优化技术。要让它生效,必须使用 ES2015 的 import/export 模块语法;在编写工具库时,尽量使用命名导出(Named Export)而非默认导出(Default Export);同时,确保代码没有副作用,或者在 package.json 中正确配置了 "sideEffects" 字段。
Q6:如何设计一个前端性能监控平台?
答 :一个完整的监控平台包含四个核心模块:数据采集(利用 Performance API 采集 LCP、CLS 等指标,捕获 JS 错误和资源加载失败)、数据上报(使用 navigator.sendBeacon 或 fetch 异步上报,注意采样率控制)、数据可视化(按页面、地域、设备维度展示趋势图和大盘数据)、告警机制(当核心指标劣化或错误率突增时,通过钉钉/邮件触发告警)。
4.浏览器缓存
浏览器缓存主要分为两大类,优先级:先强缓存,后协商缓存
强缓存Strong Cache:
命中后完全不向服务器发送网络请求,直接从本地读取缓存资源。
控制字段:
Cache-Control:
max-age=秒数(设置缓存有效期)
no-cache(跳过强缓存,走协商缓存)
no-store(完全不缓存)
public/private(控制代理服务器是否可缓存)协商缓存Negotiated Cache
当强缓存失效或未命中时触发。浏览器会向服务器发送请求,携带缓存标识,由服务器判断资源是否更新。若未更新返回
304,浏览器使用本地缓存;若已更新则返回200及新资源。控制字段:
ETag / If-None-Match(优先级高):基于文件内容生成的哈希值。能精准识别文件内容是否变化
Last-Modified / If-Modified-Since:基于资源的最后修改时间。缺点是精度只到秒,且文件内容未变但被重新保存时也会失效浏览器缓存的工作流程?
浏览器请求资源时,首先检查强缓存(
Cache-Control/Expires)。如果命中且未过期,直接使用本地缓存,不发起网络请求(状态码 200 from cache);如果未命中或已过期,则进入协商缓存阶段,携带If-None-Match或If-Modified-Since请求头向服务器验证。服务器比对后,若资源未变返回 304,浏览器使用本地缓存;若已变则返回 200 和新资源。为什么 ETag 比 Last-Modified 更可靠?
Last-Modified只能精确到秒,如果文件在 1 秒内被多次修改,它无法识别;而ETag基于内容哈希,能精准识别。如果文件内容没有改变,但被重新保存或重命名,修改时间会变化导致
Last-Modified失效,而ETag不会。某些服务器可能无法准确获取文件的最后修改时间。
项目中如何设计缓存策略?
根据资源类型分类处理:
- 静态资源(JS/CSS/图片/字体) :采用强缓存 + 文件名哈希(指纹) 。例如配置
Cache-Control: max-age=31536000(缓存 1 年),并将文件命名为app.[hash].js。这样只要内容不变,就一直走强缓存;内容一旦改变,哈希值变化,浏览器会将其视为新文件重新请求。- HTML 入口文件 用协商缓存。因为 HTML 是资源的入口,更新频繁,必须每次都向服务器验证,以保证用户能加载到最新的静态资源指纹。
- 动态接口数据:稳定数据如字典表可使用协商缓存,频繁变动数据禁用缓存
强制刷新 (Ctrl+F5)会发生什么?
强制刷新会跳过所有的强缓存和协商缓存,浏览器会直接向服务器请求最新资源。此时请求头会自动携带
Cache-Control: no-cache和Pragma: no-cache,强制服务器返回 200 和最新内容。内存缓存(Memory Cache)和磁盘缓存(Disk Cache)有什么区别?
内存缓存读取极快,但容量小,页面关闭后即失效,通常用于存放小体积或频繁访问的资源;磁盘缓存容量大、持久化,读取速度稍慢,遵循 HTTP 缓存头的规则,是大部分静态资源的归宿。
SPA缓存的幽灵旧版本问题:发版了,用户停留在旧页面没刷新,点击跳转时白屏:
原因:用户停留在旧版 HTML 页面,点击路由跳转时,前端去请求旧版本的 JS 文件(如
app.v1.js)。但服务器发版时,旧文件已经被删除,Nginx 回退返回了index.html。浏览器把 HTML 当作 JS 解析,就会报错,导致白屏解决:
前端版本检测:在 Nginx 配置一个不缓存的
/version.json文件,每次发版更新里面的版本号。前端通过路由切换时请求该文件,发现版本不一致时,提示用户"有新版本,请刷新"。
在全局拦截这类错误,并利用sessionStorage确保只自动刷新一次 (防止在网络异常时陷入死循环)Vite内置了预加载错误的事件监听,可以直接使用官方推荐的方式:
javascriptwindow.addEventListener('vite:preloadError', (event) => { // 发生预加载错误时,直接刷新页面获取最新的 index.html window.location.reload(); });SPA 中台项目的缓存配置(以 Nginx 为例)
打包后,入口文件是
index.html,所有的业务代码都被打包成了带有哈希值的静态文件(如app.a1b2c3d.js)
javascriptserver { listen 80; server_name example.com; root /var/www/spa; index index.html; # 1. 入口文件:禁用强缓存,强制每次向服务器验证 location = /index.html { add_header Cache-Control "no-cache, must-revalidate"; } # 2. 静态资源:开启长缓存(例如7天或30天),并标记为 immutable location ^~ /assets/ { add_header Cache-Control "public, max-age=604800, immutable"; } # 3. 前端路由兜底(SPA 必备) location / { try_files $uri $uri/ /index.html; } }
5.懒加载
懒加载又称为延迟加载或按需加载,是一种优化网页或应用加载时间的技术策略。它的核心思想是:将非关键资源(如图片、视频、代码块等)的加载延迟到真正需要它们的时候。在长网页中,只有当用户滚动到该资源所在的可视区域时,才会触发实际的加载请求。
首屏加速:显著减少首屏的 HTTP 请求数,缩短关键渲染路径,让用户更快看到页面核心内容。
节省资源:避免加载用户可能永远不会查看的内容,节省带宽、服务器资源及浏览器内存。
提升用户体验:减少并发加载过多资源导致的 JS 阻塞,让页面交互更流畅,降低跳出率。
懒加载原理
占位与隐藏真实地址
浏览器只有在解析到
<img>标签的src属性时才会发起请求。因此,懒加载会将真实的资源 URL 存放在自定义属性(如data-src)中,而将src指向一个极小的占位图(如 loading.gif)或留空。监听与替换
通过监听用户的滚动行为,判断资源元素是否进入了浏览器的可视区域。一旦进入,就通过 JavaScript 将
data-src的值赋给src,从而触发真实的资源加载。实现方式
随着浏览器 API 的演进,懒加载的实现方式经历了以下迭代:
原生 HTML 属性(现代推荐):现代浏览器原生支持
loading="lazy"属性。只需给<img>或<iframe>添加该属性,浏览器会自动接管延迟加载逻辑,这是目前最简洁、性能最好的方案。Intersection Observer API(现代 JS 推荐):这是目前最主流的手写懒加载方案。它不依赖高频的滚动事件,而是利用浏览器底层的异步观察者机制,高效检测元素与视口的交叉状态,性能极佳。
滚动事件监听(传统方案):通过监听
window.onscroll事件,结合getBoundingClientRect()或offsetTop计算元素位置。由于滚动事件触发极其频繁,通常需要配合防抖(Debounce)或节流(Throttle)使用,否则容易导致页面卡顿。路由懒加载(
router/index.js)
import Home from '../views/Home.vue'在路由表中直接引用是未使用懒加载使用懒加载时,路由组件通过动态的
import()函数引入
component: () => import('../views/Home.vue')在 React 中使用了
React.lazy()包裹来实现懒加载Vue 项目懒加载策略:
路由懒加载:使用
() => import()将非首屏页面拆分组件懒加载:针对弹窗、抽屉、富文本编辑器、数据图表等非首屏、重型、触发式使用的组件,使用 Vue3 的
defineAsyncComponent进行按需加载逻辑与模块的懒执行:避免在
setup顶层同步导入大模块(如 Excel 处理库、PDF 生成器)。应改在用户点击触发后,通过await import()动态加载并缓存,避免首屏加载无用逻辑。React 项目懒加载策略:Suspense 与 Error Boundary 结合
路由懒加载:使用
React.lazy配合Suspense实现路由级按需加载,显著减少初始包体积。组件懒加载:针对条件渲染的组件(如仅管理员可见的后台面板)或大型图表,使用
React.lazy包裹,并在特定交互时再渲染。异常容错处理:懒加载组件可能因网络问题加载失败,必须使用 Error Boundary(错误边界) 捕获异常,提供友好的降级 UI 或重试按钮,防止整页白屏崩溃。
前端优化点(工程化与网络层)
- Chunk 分组与命名优化(避免请求风暴)
过度细分代码会导致 HTTP 请求数量激增。在实际项目中,应通过 Webpack 的魔法注释(如
webpackChunkName)或 Vite 的manualChunks配置,将语义相关或同属一个业务模块的组件合并打包(例如将多个报表组件合并为charts.js),在减少包体积和控制请求数量之间找到平衡。
- 预加载与预取策略(Prefetch & Preload)
Preload:对当前页面核心关键资源提前拉取,避免阻塞渲染。
Prefetch:利用浏览器空闲时间,提前获取用户极可能访问的路由组件资源(如鼠标悬停在导航链接时触发低优先级 fetch)。这能实现路由切换时的"无感秒开",大幅提升用户感知性能。
- 结合 Intersection Observer 实现视口级懒加载
对于长列表中的折叠区域或滚动触达模块,仅靠路由懒加载是不够的。进阶做法是结合
Intersection ObserverAPI 监听 DOM 元素是否进入视口,当元素即将可见时才触发defineAsyncComponent或import()加载,实现真正的"按需渲染"。
- 缓存与状态分层策略
组件缓存:在 Vue 中,对高频核心组件(如 Header、Dashboard)使用
<keep-alive>进行永久缓存,对低频工具组件限制最大缓存数量(如max=5),避免内存无限增长。第三方依赖拆分:将 React/Vue 核心库、UI 组件库、Axios 等稳定依赖单独拆包(Vendor Chunk),利用浏览器的长效缓存机制,避免每次发版都重复下载。
实战避坑
首屏内容绝不懒加载:核心首页、高频页面的关键资源应同步加载,保证基础体验,避免首屏白屏或布局抖动。
解决 Loading 闪烁问题:在网络极快时,懒加载组件可能瞬间加载完成,导致
fallback(Loading 状态)一闪而过。可以通过自定义 Hook 设置最小显示时间(如 300ms),或使用 CSS 过渡动画平滑显示占位内容。SSR 环境下的兼容:在服务端渲染(SSR)项目中,懒加载组件在服务端默认不执行。需配合
ssr: false或no-ssr标签避免水合(Hydration)异常,或者使用Loadable Components等第三方库实现同构加载。图片懒加载的实现原理?
先将图片的真实地址存放在
data-src属性中,src属性设为占位图。然后通过IntersectionObserverAPI 监听这些图片元素。当图片进入浏览器可视区域时,触发回调函数,将data-src的值赋给src,完成图片的真实加载。加载完成后,停止对该元素的观察。懒加载和预加载(Preload)有什么区别?
两者的目的都是提升用户体验,但策略完全相反。
懒加载(Lazy Load):是"按需分配",延迟加载非可视区域的资源,能减少初始请求,缓解服务器压力。
预加载(Preload):是"提前准备",在资源使用前就提前请求并放入缓存。当真正使用时直接从缓存读取,适用于九宫格抽奖等对切换速度要求极高的场景,但会增加初始的网络和服务器压力。
懒加载对 SEO 有影响吗?
传统的滚动监听懒加载可能会导致搜索引擎爬虫无法触发滚动,从而无法抓取和索引懒加载的内容,对 SEO 产生负面影响。
解决方案:使用原生的
loading="lazy"属性或IntersectionObserverAPI,现代搜索引擎爬虫已经能够较好地识别这些标准实现。此外,务必为图片添加准确的alt文本和语义化标签,确保爬虫能获取到关键信息。除了图片,懒加载还有哪些应用场景?
路由级懒加载:在 SPA(单页应用)中,通过动态
import()语法按需加载路由组件,极大减小首屏 JS 包体积。长列表/大数据渲染:结合虚拟列表(Virtual List)技术,只渲染可视区域内的 DOM 节点,解决万级数据渲染导致的浏览器卡顿问题。
第三方组件/重型库懒加载:如富文本编辑器、图表库等,在用户触发特定交互时才去加载相关代码。
手写懒加载如何保证良好的用户体验?
在资源加载完成前,应使用轻量级的占位符(如低分辨率预览图 LQIP、骨架屏 Skeleton Screen 或纯色背景)来防止页面布局突然变化(CLS)。同时,需要处理加载失败的情况,提供重试机制或错误兜底图,确保用户体验不受损。
6.预加载Preload&预取Prefetch
预取(Prefetch)和预加载(Preload)是前端性能优化中非常重要,但也极易混淆的两个概念。它们的核心目的都是"提前获取资源,减少用户等待时间",但优先级、触发时机和应用场景完全不同。
对比维度 预加载(Preload) 预取(Prefetch) 目标资源 当前页面必需的关键资源 未来导航可能用到的资源 优先级 极高(抢占带宽) 极低(空闲时执行) 资源类型 字体、首屏大图、核心JS/CSS 下一个路由的 JS Chunk、下一页 HTML 核心目的 消除当前页面的加载阻塞 实现未来页面的"秒开" 预加载(Preload)
Preload 之所以能带来显著的性能提升,核心原因不在于它能让网络下载变得"更快",而在于它打破了浏览器默认的"资源发现与加载的串行依赖",把原本串行的工作变成了并行工作。
不使用 Preload:串行阻塞(瀑布流效应)
首屏需要一张关键大图(LCP 元素),而这张图片是在 CSS 文件中通过
background-image引用的。
HTML 解析:浏览器下载并解析 HTML,发现需要请求 CSS 文件。
CSS 下载:浏览器发起网络请求下载 CSS 文件。
CSS 解析:CSS 下载完成,浏览器开始解析 CSS 规则。
图片发现:解析到
background-image时,浏览器此时才发现需要这张图片。图片下载:浏览器才发起网络请求去下载这张图片。
图片的下载被严重延后了。它必须等待 HTML 下载完、CSS 下载完、CSS 解析完才能开始。这就形成了严重的"请求瀑布流(Waterfall)",导致首屏渲染时间(LCP)被大幅拉长。
使用 Preload:并行下载(消除瀑布流)
如果你在
<head>中加了<link rel="preload" href="hero.jpg" as="image">,流程会变成这样:
HTML 解析:浏览器下载并解析 HTML,在
<head>中第一时间发现了 Preload 指令。并行下载:浏览器立刻以高优先级发起对
hero.jpg的网络请求。同时,它也在后台下载 CSS 文件。CSS 解析:CSS 下载并解析完成,渲染引擎准备绘制首屏。
命中缓存:渲染引擎发现需要
hero.jpg,去缓存中一找,发现 Preload 早就把它下载好放在内存里了!直接读取,无需再次发起网络请求。图片的下载与 CSS 的下载是同时(并行)进行的。图片的下载时间被"提前"了,省去了等待 CSS 下载和解析的漫长耗时。
Preload 并没有缩短网络传输的物理时间,但它提前了下载开始的起跑线,让关键资源能和主文档一起"赛跑",从而实现了首屏的极速渲染。
① 在 HTML 中声明(最推荐用于字体和首屏大图)
在
<head>标签中使用<link rel="preload">,必须指定as属性告诉浏览器资源类型
javascript<!-- 预加载自定义字体,消除文字闪烁(FOIT/FOUT) --> <link rel="preload" href="custom-font.woff2" as="font" type="font/woff2" crossorigin="anonymous"> <!-- 预加载首屏核心大图 --> <link rel="preload" href="hero-image.jpg" as="image">② 在 JavaScript 中动态预加载(用于动态路由或按需加载的组件)
javascript// 当用户进入某个页面时,提前预加载一个稍后才会用到的重型组件 const preloadHeavyComponent = () => { const link = document.createElement('link'); link.rel = 'preload'; link.href = '/assets/heavy-chart.js'; link.as = 'script'; document.head.appendChild(link); };预取(Prefetch)的实战做法
① 结合构建工具(Webpack/Vite)实现路由预取
这是 SPA 项目中最常用的手段。当用户停留在首页时,利用网络空闲提前下载其他页面的代码
javascript// Webpack 魔法注释:浏览器空闲时预取用户中心页面的代码 const UserView = () => import(/* webpackPrefetch: true */ '../views/UserView.vue'); // Vite 项目同样支持动态 import 配合特定的插件实现预取② 在 HTML 中声明(用于传统多页应用或明确的外部链接)
javascript<!-- 预取下一页的内容 --> <link rel="prefetch" href="/api/next-page-data">③ 现代浏览器的新标准:推测规则 API(Speculation Rules API)
这是目前最前沿的预取方案,允许你以声明式的方式告诉浏览器预取或预渲染哪些链接
javascript<script type="speculationrules"> { "prefetch": [{ "source": "list", "urls": ["/next-page.html", "/search-results.html"] }] } </script>避坑指南
切忌滥用 Preload:Preload 是高优先级的,如果预加载了当前页面根本不需要的资源,反而会阻塞真正关键资源的下载,导致首屏变慢。
注意移动端弱网环境:在移动端或弱网环境下,过度使用 Prefetch 会消耗用户的流量和电量。现代浏览器在用户开启"省电模式"或"流量节省模式"时,会自动忽略 Prefetch 请求。
Preload 的 CORS 问题:如果预加载的是跨域资源(尤其是字体),必须加上
crossorigin="anonymous"属性,否则浏览器会下载两次(一次预加载,一次实际使用)。缓存策略的配合:预取回来的资源,依然要遵循我们之前聊过的 HTTP 缓存策略。如果预取的资源没有设置合理的
Cache-Control,依然无法发挥"秒开"的威力。一句话总结:Preload 用来保当前页面的首屏极速渲染,Prefetch 用来保未来页面的无缝切换。