前端性能优化方案梳理

259 阅读10分钟

合理利用缓存

  • 使用CDN:让用户访问最近的资源,减少来回传输时间;

    • CDN 的核心点有两个,一个是缓存,一个是回源。“缓存”就是说我们把资源 copy 一份到 CDN 服务器上这个过程,“回源”就是说 CDN 发现自己没有这个资源(一般是缓存的数据过期了),转头向根服务器(或者它的上层服务器)去要这个资源的过程。
    • CDN预热:提前将CDN部署到全国各个节点
    • CDN刷新:强制CDN拉取服务器最新资源
  • 浏览器缓存:

    • Memory Cache:Memory Cache缓存的大部分是preloader指令下的静态资源。我们发现,Base64 格式的图片,几乎永远可以被塞进 memory cache。

    • Service Worker Cache:

    • 使用service worker通常用来做缓存文件,提高首屏速度developers.google.com/web/ilt/pwa…

      • 可以直接使用 workbox 的第三方库更方便的缓存资源,这是谷歌提供的一个工具库。
    • HTTP Cache:做HTTP缓存(强缓存与协商缓存:添加Expires头和配置Etag)用户可以重复使用本地缓存,减少对服务器压力;

    • Push Cache:推送缓存,它是HTTP2.0新增加的内容

  • 离线存储manifest:告知浏览器被缓存的内容,按照manifest文件的规则,将文件保存在本地,从而在没有网络链接的情况下,也能访问页面。

只请求当前需要的资源

  • 异步加载,懒加载
  • 图片预加载
  • 按路由拆分代码,按需加载:
    • 代码分割可以通过webpack的splitChunk提取公共代码,核心意义在于异步加载资源,同时避免重复打包以提升缓存利用率,进而提升访问速度。

    • 将不常变化的第三方依赖进行代码拆分,方便对第三方依赖库缓存,同时抽离公共逻辑,减少单个文件size大小。

缩减压缩包体积

  • 合并压缩CSS、js、图片、静态资源

  • 服务器开启GZIP

时序优化

  • Promise.all 并发执行
  • SSR(服务端做好所有渲染,好处是可以做缓存,方便SEO(资源直接输出html))
  • Prefetch:
    • Prefetch包括资源预加载、DNS预解析(DNS-Prefetch)、http预连接(Preconnect)、预渲染(rel="prerender")
  • css放顶部,js放底部(css可以并行下载,而js加载之后会造成阻塞)

降低请求量

  • 合并资源
  • 减少 HTTP 请求数
  • minify / gzip 压缩:
  • webP
  • lazyload
  • 开启KeepAlive:
    • 减少浏览器与服务器建立连接的次数,从而节省建立连接时间

    • 开启KeepAlive也会使服务器负载变大,也更加容易遭受攻击,实际项目中需要权衡利弊

加快请求速度

    • 为了增加浏览器下载的并行度,让浏览器能同时发起更多的请求,比如将基准网页中的JS、CSS和图片分别使用三个域名加载,分别是[http://js.perfma.com](http://js.perfma.com)、[http://css.perfma.com](http://css.perfma.com)、[http://img.perfma.com](http://img.perfma.com)
    • 域名拆分为3到5个比较合适,过多的域名会带来DNS解析时间的损耗,可能会降低性能
  • CDN 分发
  • 启动HTTP 2.0:
    • http2 vs http1 demo http2.akamai.com/demo

    • 多路复用:可以用同一个连接传输多个资源。http 1.1会让资源排队加载,而http2.0资源都是同时加载的,后面加载的资源不需要进行排队。

渲染优化

  • 服务端渲染:无需等待js文件下载执行的过程
  • 使用加载占位符:在白屏无法避免的时候,为了解决等待加载过程中白屏或者界面闪烁
  • 虚拟列表优化
  • 请求以及资源缓存
    • 在一些前端系统中,当加载页面时会发送请求,路由切换出去再切换回来时又会重新发送请求,每次请求完成后会对页面重新渲染。然而这些重新请求再大多数时是没有必要的,合理地对 API 进行缓存将达到优化渲染的目的。原理是对每一条 GET API 添加 key,根据 key 控制该 API 缓存,重复发生请求时将从缓存中取得。比如使用react-query

LCP 优化

LCP:Largest Contentful Paint,当前视窗中的最大内容元素渲染时间,小于2.5s属于还不错。
元素大小是指元素在视口中的可见大小,如果一个元素延伸到视口外面,如果有裁剪或者延伸到视口外面,则不纳入计算。如果元素是图片,图片固有尺寸大小和实际渲染大小,则取最小值那个。
LCP主要受以下四方面影:

  • 较慢的服务器响应时间

  • 阻塞渲染的js或者css

  • 资源加载时间

  • 客户端渲染性能

可以使用web-vitals测量LCP时间。关于LCP的优化方法参考如下:

  • 降级:SSR+SSG(不带数据的SSR):Static site Generation。

  • 调整资源加载优先级
    • LCP内容元素优先被浏览器渲染
    • 首屏图片webp
  • 关键渲染路径优化:内联首屏css(style标签包裹起来)=> DOM结构 => 核心业务js=>非核心业务js

  • service worker:使用Google workbox做离线缓存

  • 首屏图片直出,非首屏懒加载

相关参考:web.dev/optimize-lc…

FID 优化

First Input Delay,FID衡量的是用户输入到主线程空闲的时间差。

关于FID的优化方法参考如下:

FID减少长任务

  • 延迟执行js:避免阻塞渲染
  • 减少文件体积(合理拆包,code split)
    • 不同入口文件分别拆分各自vendor.js
  • 打断长任务(执行时间超过50ms)
    • 利用事件循环机制,拆分长任务,放到任务队列。初始化任务可能是串行的,例如函数嵌套函数,很容易引发长任务,可以利用requestIdleCallback,在浏览器空闲的时候去执行。可以引入idlize包,打断长任务。
  • 耗时任务放web worker

  • WASM(js 性能低下,C++/Rust 高性能)

  • react fiber:解决高优先级任务不能被优先执行,核心是将任务拆成多个子任务,保证子任务不会出现long task

相关参考:web.dev/optimize-fi…

使用webpack 优化项目

  • 按照路由拆分代码,实现按需加载

  • 使用ES6模块开启tree shaking

  • 优化图片,小图可以使用base64方式写入文件

  • 给打包出来的文件名添加哈希,实现浏览器缓存文件

  • 使用webpack-bundle-analyzer分析js size:

使用Performance分析优化

Chrome Devtools 的 Performance 工具是网页性能分析的利器,它可以记录一段时间内的代码执行情况,比如 Main 线程的 Event Loop、每个 Event loop 的 Task,每个 Task 的调用栈,每个函数的耗时等,还可以定位到 Sources 中的源码位置。

Performance 工具最重要的是分析主线程的 Event Loop,分析每个 Task 的耗时、调用栈等信息。
因为渲染和 JS 执行都在主线程,在一个 Event Loop 中,会相互阻塞,如果 JS 有长时间执行的 Task,就会阻塞渲染,导致页面卡顿。所以,性能分析主要的目的是找到 long task,之后消除它。

使用Performance分析性能瓶颈

有如下代码,点击block1,调用myClickEvent,myClickEvent同步执行forEach循环,之后延迟1.5s调用changeHeight函数,改变block1的高度。

//index.html 
<div class="block1">block1</div>
<div class="block2">block2</div>
// script.js
const block1 = document.querySelector('.block1');
const block2 = document.querySelector('.block2');

function changeHeight() {
  block1.style.height = '300px';
}
function arrayFun() {
  console.log('1');
}
function myClickEvent() {
  setTimeout(() => {
    changeHeight();
  }, 1500);
  new Array(10000).fill(0).forEach(arrayFun);
}
block1.addEventListener('click', myClickEvent);


如上图所示,我们可以在main 主线程中看到带有红色三角号的task,说明该任务是个long task,存在性能问题。点击该task,可以在下方的event log看到具体的事件日志。


根据上图,找到self time耗时最长的任务,右侧可以看到具体的js文件和行数,这样便定位到问题所在。

使用Performance分析代码使用率

我们还可以通过 Coverage 工具分析运行时的代码使用情况:

小结:资源加载的性能优化可以用 Coverage 工具记录代码使用情况,分析出没用到的代码,使用 treeshking、懒加载等方式,针对性的优化它。

使用Chrome 排查内存泄露

内存泄露是指由于疏忽或错误造成程序未能释放已经不再使用的内存。

重现您认为可能正在泄漏的某些场景,例如,打开和关闭模式对话框。对话框关闭后,您希望内存恢复到上一级。因此,您需要拍摄另一个快照,然后将其与上一个快照进行比较。

  • Chrome DevTools —— Memory—— snapshot。打开ui,关闭ui,重复几遍,查看内存使用量较之前是否变大,如果是的话,说明关闭UI没有释放不再使用的内存变量。

  • 其次是Chrome的task manager,可以看到独立的tab的内存使用情况,如果存在内存泄露,则随着用户的操作,该tab的内存会不断增大。

developers.google.com/web/tools/c…

使用Network分析网络请求

Reference:

React 组件性能优化

  • 减少计算量,优化render过程:
    • useMemo和useCallback实现稳定的Props值:如果传递给子组件的派生状态或函数每次都是新的引用,那么PureComponent和react.memo就会失效
    • 使用PureComponent、React.memo优化子组件渲染,避免子组件重新render
    • 使用useMemo减少组件render过程耗时。避免每次渲染时进行高开销的计算
    • 将多个state合并为单个state:
      • 比如useState传一个对象,对象包含多个属性来替代单个属性state。
      • 使用react官方提供的unstable_batchedUpdates方法,将多个setState封装在unstable_batchedUpdates回调中(不推荐)
  • 简化props
    • 请只传递component需要的props,避免其它props变化导致重新渲染(慎用spread attributes。传得太多,或者层次传得太深,都会加重shouldComponentUpdate里面的数据比较负担)
  • 延迟加载不是立即需要的组件:使用新的React.LazyReact.Suspense轻松完成
  • 使用函数式组件而不是类组件
  • 不要在渲染函数中进行不必要的计算
    • 比如不要在渲染函数(render)中进行数组排序、数据转换、订阅事件、创建事件处理器等等。渲染函数中不应该放置太多副作用)
  • 减少不必要的节点嵌套
    • 一般不必要的节点嵌套都是滥用高阶组件/RenderProps 导致的。 有很多种方式来代替高阶组件/RenderProps,例如优先使用 props、React Hooks
  • 避免使用箭头函数形式的事件处理器
    • 箭头函数每次渲染时都会创建一个新的事件处理器,这会导致 Component 始终会被重新渲染.
  • 精细化渲染:只有一个数据来源导致组件重新渲染, 比如说 A组件 只依赖于 a 数据,那么只有在 a 数据变动时才渲染 A组件,其他状态变化不应该影响组件 A组件。对于Mobx 来说,越细粒度的组件,可以收获更高的性能优化效果
  • 组件卸载前进行清理操作
    • 比如:在组件中为 window 注册的全局事件,以及定时器,在组件卸前要清理掉,防止组件卸载后继续执行影响应用性能。
  • 为列表数据添加唯一标识
    • key用于识别唯一的虚拟DOM元素。保持key值的稳定性很重要,因为如果key发生了变化,react则会重新触发UI重渲染。可以避免元素因为位置变化而导致的重新创建。
  • 避免Context导致子组件不必要的重复渲染

  • 使用React Profiler分析组件渲染耗时过程:Profiler 只记录了Render过程耗时

了解最新的React Fiber架构


应该知道什么是fiber算法,Fiber架构,双缓冲模式,Lane模型,时间切片,Reconciler,Scheduler,和Concurrent模式。

相关阅读