Web前端性能优化——实践篇

·  阅读 2636
Web前端性能优化——实践篇

前言

Web前端性能优化——了解篇了解了一些前端性能的知识,本篇文章将主要讲如何去做好性能优化。

在进行性能优化前,我们需要设计好性能监控方案

性能监控方案

对于一个Web应用而言,监控会分为成 后端监控前端监控 两块去做.

后端监控 也会分为多个模块,比较常见的有 服务器性能监控接口性能监控

前端监控 一般是针对浏览器访问页面的性能,客户端访问页面的性能主要分为加载、内容呈现和用户交互。如果是 SSR 应用,也需要监控服务器的性能。

如果服务端不是使用NodeJS,一般不需要前端开发人员去处理监控方案,因此前端一般需要处理的时候 Node.js性能监控浏览器性能监控

Node.js性能监控

Node.js性能监控也是监控服务器的性能,相关的性能指标介绍可以去看一下这一篇文章:一篇文章带你了解Node.js的性能指标

在Node.js可以使用 prom-client进行数据服务器性能采集和上报:

function startPromCollect({ jobName }) {
  // 引入性能检测
  const promClient = require("prom-client");
  const address = require("address");
  // pushgateway 地址
  const pushIp = process.env.BUILD_ENV === "prod" ? "http://xxx" : "http://xxx";

  const gateway = new promClient.Pushgateway(pushIp);
  // 收集默认数据
  promClient.collectDefaultMetrics();
  // 30秒push一次
  setInterval(() => {
    gateway.push({ jobName: jobName, groupings: { instance: address.ip(), pid: process.pid } }, (err, resp, body) => {
      if (err) {
        console.error(`prometheus Pushgateway pushError: ${err}`);
      }
    });
  }, 30000);
}

// 开发环境不进行上报
if (process.env.BUILD_ENV !== "dev") {
  // 开始性能检查
  startPromCollect({
    jobName: `${process.env.BUILD_ENV}-my-web-app`,
  });
}
复制代码

这只是前端采集和上报的代码,还需要实现的对应的数据分析平台,可以参考官方文档自己搭建一个 基于Prometheus 的 Grafana 性能分析平台 ,也可以参考这篇文章:使用Prometheus + Grafana搭建监控系统

浏览器性能监控方案

可以根据[Web前端性能优化——了解篇]介绍的指标计算方法实现自己的 性能监控库,性能数据可以上报到 Grafana性能分析 平台,实时分析性能数据,也可以直接接入 google 的 firebase,不过 firebase 会有一些延迟,但数据会保存三个月。如果不需要自定义性能指标,直接使用 firebase 上报默认的指标即可,firebase接入文档:firebase.google.com/docs/web/se…

性能优化

接下来我们就根据性能指标分类来分析性能优化方案。文档加载相关指标 并不能反映用户体验, 内容呈现指标 基本也包含 文档加载相关指标,因此 文档加载内容呈现 会合并一起进行分析。

加载或内容呈现性能优化

加载性能主要影响因素有: 资源响应速度资源体积优化资源加载的顺序代码质量用户网络速度用户设备条件 ,不过用户网速和设备我们无法控制,所以我们主要优化方向是其他几个方面

资源响应速度

资源响应速度的主要优化点在于:减少请求数、减少请求资源体积、提升网络传输效率

  • 使用 CDN 加速:利用CDN增加并发连接和长缓存的优势来加速下载静态资源
  • 开启gzip压缩:使用 gzip 压缩编码技术,减小资源体积。
  • 浏览器缓存:利用浏览器缓存(强缓存与协商缓存)与 Nginx 代理层缓存,缓存静态资源文件。
  • 减少网络请求次数和体积:通过压缩文件及合并小文件为大文件,减少网络请求次数,但需要找到合理的平衡点。
  • 使用 HTTP/2 :HTTP/2带来的的优势可以看这一篇文章:解读 HTTP1/HTTP2/HTTP3
CDN加速

内容分发网络(CDN)是一组分布在多个不同地理位置的 Web 服务器。我们都知道,当服务器离用户越远时,延迟越高。CDN 就是为了解决这一问题,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。CDN的优势可以去看CDN 原理

gzip压缩

gzip 编码是改进 web 应用程序性能的技术,通常web 服务器和客户端(浏览器)必须同时支持 gzip。gzip 压缩效率非常高,通常可达 70% 压缩率。

webpack 中可以使用 CompressionWebpackPlugin 插件进行gzip压缩:

if (process.env.NODE_ENV === 'production') {
  plugins.push(new CompressionWebpackPlugin({
    filename: '[path].gz[query]',
    algorithm: 'gzip',
    test: new RegExp(`\.(${productionGzipExtensions.join('|')})$`),
    threshold: 10240,
    minRatio: 0.8,
    deleteOriginalAssets: false
  }))
}
复制代码

服务端支持 gzip,以 Nginx 配置为例:

gzip on;
gzip_static on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_comp_level 2;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/png image/jpeg image/svg+xml image/gif;
gzip_vary off;
gzip_disable "MSIE [1-6].";
复制代码

服务器支持gzip压缩后,response header 中会显示 Content-Encoding: gzip。

为何要同时使用webpack gzip压缩和 服务的gzip压缩 呢? 答案可以看一下这里: webpack gzip压缩 和 nginx gzip压缩的区别

  • 之所以在 webpackTerserPlugin 插件已对文件进行压缩的结果下,还进行一次 gzip 压缩,是因为 gzip 能够在已压缩文件的基础上,再次进行压缩
  • 之所 webpacknginx 都对静态资源进行 gzip 压缩,是为了让 nginx 能够优先使用静态 gzip 压缩,直接使用 gz 文件的结果作为 gzip 压缩的结果,从而减少实时 gizpcpu 资源的占用

如何开启双端gzip压缩,也可以看这里:「简明性能优化」双端开启Gzip指南

浏览器缓存

前端缓存一般可分为 http缓存和浏览器缓存,http缓存还分强缓存协商缓存,如果想对前端缓存了解更多,可以去看一下这本专栏 前端缓存技术与方案解析

强缓存生产方式:

注意点:浏览器请求资源一般会默认开启强缓存,协商缓存生效的前提是强缓存失效,一般关闭强缓存的方式可以是设置资源响应头:

Cache-Control: max-age=0
复制代码

协商缓存生效过程:

前面介绍完了强缓存和协商缓存,现在来说一下 HTTP 缓存的常见方案,也算是通用方案:

  • 频繁变动的资源,比如 HTML, 关闭强缓存,始终采用协商缓存
  • CSS、JS、图片资源等采用强缓存,因为资源请求默认都会采用强缓存,但如何保证有资源有变更的时候不会读取缓存呢,那就是使用 hash 命名,这里就要借助比如类似webpack的工具,在打包的时候,根据文件内容来生成文件,并把资源链接动态插入HTML中。具体如何如何使用可以去看 webpack缓存方案

浏览器缓存按照缓存位置划分可以分为:

  • Service Worker Cache:本质上是一种用 JavaScript 编写的脚本,其作为一个独立的线程,它可以使应用程序能够控制网络请求,缓存这些请求以提高性能,并提供对缓存内容的离线访问。而常见的 Service Worker 应用就是渐进式 Web 应用(Progressive Web Apps)
  • Memory Cache 内存缓存:计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。
  • Disk Cache 磁盘缓存:读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。
  • Push Cache 推送缓存:HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。

http缓存就是利用浏览器的 Memory Cache 和 Disk Cache 两种缓存方式,原理就是浏览器在和服务端进行交互时,添加了一层拦截器,这层拦截器来进行缓存喝是否需要返回缓存内容,也会根据不同情况来判断使用 Memory Cache 还是 Disk Cache 。

减少网络请求次数和体积
  • 合理使用 webpack 打包策略进行代码拆包,参考 代码分离
  • 图片精灵(升级HTTP/2后不建议使用)
  • 清理多余js/css代码
  • 图片转base64策略优化,太大的突破不要使用base64,base64体积会更大,且影响js体积
使用HTTP/2

HTTP/2 相对于HTTP/1来说,进行了一些列的增强:

  1. 多路复用,解决TCP协议慢启动问题:主要因为HTTP/1 每个请求都会新建一个TCP连接,每个域名同时最多同时有6个TCP连接,会造成 TCP 连接之间相互竞争带宽,而且启动TCP连接相对较慢。
  2. 可以设置请求的优先级:可以在发送请求时,标上该请求的优先级,这样服务器接收到请求之后,会优先处理优先级高的请求。
  3. 头部压缩:无论是 HTTP/1.1 还是 HTTP/2,它们都有请求头和响应头,这是浏览器和服务器的通信语言。HTTP/2 对请求头和响应头进行了压缩,你可能觉得一个 HTTP 的头文件没有多大,压不压缩可能关系不大,但你这样想一下,在浏览器发送请求的时候,基本上都是发送 HTTP 请求头,很少有请求体的发送,通常情况下页面也有 100 个左右的资源,如果将这 100 个请求头的数据压缩为原来的 20%,那么传输效率肯定能得到大幅提升。

HTTP/2带来的的优势也可以看这一篇文章:解读 HTTP1/HTTP2/HTTP3

资源体积优化

不同资源类型优化方式不一样,需要针对各种类型的资源去做响应的优化

文本资源

文本资源包含 HTMLCSSJS 等,主要优化手段:

  • 代码压缩:minify
  • 压缩内容:比如使用 gzip 压缩
  • 代码精简
  • JS 体积优化方案
    • Tree Shaking
    • Code Split
    • 组件按需加载
    • 代码按需打包
  • CSS 体积优化方案
    • 引入第三方库样式文件时按需引入
    • 减少不必要的 css 前缀补全
图片资源

一个页面,图片资源的大小一般占据整个页面资源体积的一半以上,因此图片资源体积优化也非常重要。

图片体积优化的手段:

  • 去掉不必要的图片,能使用样式实现的不要使用图片
  • 雪碧图(HTTP/2及以上不需要雪碧图)
  • 上传图片大小限制
  • 压缩项目静态图片
  • 接入Webp图片处理,可以根据浏览器请求中所带的 accept 来判断是否支持webp格式,各cdn厂商基本上也都支持webp图片转换:阿里云图像处理

资源加载的顺序

从输入url到页面完全加载 中我们讲解了页面呈现的过程,因此可以知道资源加载是有顺序的,下面来看一下加载阶段的渲染流水线:

可以知道并非所有的资源都会阻塞页面的首次绘制,比如图片、音频、视频等文件就不会阻塞页面的首次渲染;而 JavaScript、首次请求的 HTML 资源文件、CSS 文件是会阻塞首次渲染的,因为在构建 DOM 的过程中需要 HTML 和 JavaScript 文件,在构造渲染树的过程中需要用到 CSS 文件。

我们把这些能阻塞网页首次渲染的资源称为关键资源,因此我们需要梳理清楚哪些是关键资源,哪些是非关键资源。资源加载顺序往往于代码所在位置或者设置的属性有关:

  1. 一般我们需要把css 放在 header 中,以便于页面渲染出来时,页面能按照预期中的样式正常显示。
  2. js 代码一般放在 DOM 底部,如果 JavaScript 文件中没有操作 DOM 相关代码,就可以将该 JavaScript 脚本设置为异步加载,通过 asyncdefer 来标记代码; asyncdefer 虽然都是异步的,不过还有一些差异,使用 async 标志的脚本文件一旦加载完成,会立即执行;而使用了 defer 标记的脚本文件,需要在 DOMContentLoaded 事件之前执行。

代码质量

代码质量分为很多方面,比如代码量、复杂度、代码结构设计等等

代码量

代码量优化的方案主要分为一下几种:

  • 代码精简:使用简洁并清晰的代码编写,这个一般与开发者的工作经验或者知识面有很大的关系
    • 使用 lodash 提供的功能函数
    • 使用正则替代一些复杂的js校验或者匹配功能
    • 合理使用一些位运算符
    • 使用es6语法
    • 去除无效代码
  • 抽离并封装公用模块代码
    • 当一个功能被多次使用就应该封装成公共函数
    • 公共组件封装
  • css原子化,尽量让每一行css都能得到充分利用
代码复杂度设计

复杂度主要分为 时间复杂度空间复杂度

时间复杂度 对性能的影响在于:增加 js 解析时间,主要主要优化手段有以下几种:

  • 减少嵌套循环,使用空间换时间
  • 使用高性能算法处理复杂功能

空间复杂度 对性能的影响在于:占据设备内存过大时,可能引起浏览器崩溃等问题,主要主要优化手段:

  • 减少全局变量,和注意全局变量所占内存,防止内存不断增大,导致内存溢出。
  • 注意销毁不需要的对象,防止不销毁旧的对象,又不断生成新的对象,页面所在内存持续增长,导致页面崩溃。
代码结构设计

好的代码结构设计,对性能的提升影响会特别大,主要的一些设计手段有以下几种:

  1. 组件懒加载:让页面首次加载只渲染首屏展示的内容,可以提升首屏生产内容的速度和DOM解析显示到页面的速度,当DOM节点过多,对于浏览器的渲染进程的压力也会越大,显示到页面的时间就会长
  2. virtual-list : 当长列表页面上拉加载越来越多的内容时,DOM节点会不断增大,就会造成每次生产新图层会越来越久,就会出现渲染卡顿、内存增大等问题,因此可以使用virtual-list方案只给DOM添加当前屏幕显示的DOM节点,可以避免出现渲染卡顿等问题。
  3. 图片懒加载:当页面图片太多,同时请求会对服务端造成一定的压力,也可以防止并发加载的资源过多而阻塞js 或者其他关键资源的加载。
  4. css 对性能的优化方案:可参考[仅使用CSS提高页面渲染速度](juejin.cn/post/694266…

交互相关性能优化

影响交互性能的主要有几方面: 操作响应速度页面流畅度交互体验设计

操作响应速度

什么情况会影响操作的响应速度?

  • 操作后执行时间过长,用户等待时间长
  • 有任务正在执行,占据主线程,需要等待主线程空闲

我们可以从以下几个方面进行优化操作响应的速度:

  • 首次加载只执行首屏需要的代码,非首屏需要的代码可以按需加载
  • React 可以开启 Concurrent 模式来实现可中断渲染,优先处理用户操作:Concurrent 模式介绍
  • 当需要执行一段逻辑复杂、耗时较长的代码时,如果不是一定需要阻塞其他应用,可以利用 requestIdleCallback来在空闲时间再继续执行代码,或者使用异步或者使用定时器让任务在下一个Eventloop执行
  • 优化代码执行时间

页面流畅度

页面不流畅的主要原因有 两大类 原因:

  • 渲染不及时,页面掉帧
  • 页面内存占用过高,运行卡顿
渲染不及时,页面掉帧

主要原因:

  • 长时间占用js线程:js任务过长
  • 页面回流和重绘较多:需要去排查是否有代码会频繁操作 DOM 或者频繁获取 offsetTopoffsetLeftoffsetWidthoffsetHeightscrollTopscrollLeftscrollWidthscrollHeightclientTopclientLeftclientWidthclientHeight 等需要通过即时计算得到的属性。
  • 资源加载阻塞
页面内存占用过高,运行卡顿

主要原因:

  • 意外的全局变量引起的内存泄漏
  • 闭包引起的内存泄漏
  • 被遗忘的定时器
  • 循环引用
  • DOM删除时没有解绑事件
  • 没有清理的DOM元素引用

交互体验设计

交互一般包括:操作、视觉变化、使用引导、用户习惯性行为等等

比如一般我们都会按照ui出的设计稿进行编写页面,但是ui设计有时候不会注意一些细节,比如弹框显示或隐藏的过渡时间或者效果,友好的过渡效果,不会让用户觉得弹出出现的太突兀。因此我们也许要考虑交互的合理性。

参考资源

分类:
前端
收藏成功!
已添加到「」, 点击更改