前端性能如何优化?

本篇文章参考以下博文

前言

  最近在学习过程中突然看到这个问题,也想总结以下自己关于性能优化的理解,这个问题比较大,我们需要逐步解耦来处理,回答需要有条理,有逻辑,才不至于涵盖面不全,有遗漏点。

  首选我们要弄清楚一个问题,为什么要性能优化?或者说性能优化的目的是什么?那肯定是为了快速响应客户的操作。那么怎么样才能实现快速响应呢?可以从两方面来考虑,其一,提高页面加载时的速度,其二,提高页面运行时的速度。

  除了上面两个方面,我个人觉得还可以从减少用户对网络延迟的感知方向入手。让用户减少不必要的 UI 感知。

一、减少延迟感知

  这里我们以业界人机交互最顶尖的苹果举例,在 IOS 系统中:

  点击“设置”面板中的“通用”,进入“通用”界面:

在这里插入图片描述

  作为对比,再点击“设置”面板中的“ Siri 与搜索”,进入“ Siri 与搜索”界面:

在这里插入图片描述
  你能感受到两者体验上的区别么?

  事实上,点击“通用”后的交互是同步的,直接显示后续界面。而点击“ Siri 与搜索”后的交互是异步的,需要等待请求返回后再显示后续界面。但从用户感知来看,这两者的区别微乎其微。

  这里的窍门在于:点击“ Siri 与搜索”后,先在当前页面停留了一小段时间,这一小段时间被用来请求数据。

  当“这一小段时间”足够短时,用户是无感知的。如果请求时间超过一个范围,再显示 loading 的效果。

  试想如果我们一点击“ Siri 与搜索”就显示 loading 效果,即使数据请求时间很短, loading 效果一闪而过。用户也是可以感知到的。

  这方面的可以参考下 React hook ——useDeferredValue

二、提高页面加载速度

2.1 减少 HTTP 请求

  我们知道一次 HTTP 请求需要经历, DNS 查找, TCP 握手,浏览器发出 HTTP 请求,服务器接收请求,服务器处理请求并发回响应,浏览器接收响应等过程。以百度为例子,看一下一次请求的事件都花费在了哪:
在这里插入图片描述

  名词解释:

  • Queueing : 在请求队列中的时间。
  • Stalled : 从TCP 连接建立完成,到真正可以传输数据之间的时间差,此时间包括代理协商时间。
  • Proxy negotiation : 与代理服务器连接进行协商所花费的时间。
  • DNS Lookup : 执行DNS查找所花费的时间,页面上的每个不同的域都需要进行DNS查找。
  • Initial Connection / Connecting : 建立连接所花费的时间,包括TCP握手/重试和协商SSL。
  • SSL : 完成SSL握手所花费的时间。
  • Request sent : 发出网络请求所花费的时间,通常为一毫秒的时间。
  • Waiting(TFFB) : TFFB 是发出页面请求到接收到应答数据第一个字节的时间。
  • Content Download : 接收响应数据所花费的时间。

  上面这写过程,是一次 HTTP 请求需要消耗的事件,那我们为了提高性能,尽量把一些相关的小文件,合并为一个大文件,用一次请求发送过来。

2.2 使用 HTTP2

  为了加快响应速度,最直接的方法就是升级 HTTP ,目前最新的 HTTP2.0 ,相比之前,很多方面都进行了优化处理。

   HTTP2 相比 HTTP1.1 有如下几个优点:

   1.解析速度快

  服务器解析 HTTP1.1 的请求时,必须不断地读入字节,直到遇到分隔符 CRLF 为止。而解析 HTTP2 的请求就不用这么麻烦,因为 HTTP2 是基于帧的协议,每个帧都有表示帧长度的字段。

   2.多路复用

   HTTP1.1 如果要同时发起多个请求,就得建立多个 TCP 连接,因为一个 TCP 连接同时只能处理一个 HTTP1.1 的请求。

  在 HTTP2 上,多个请求可以共用一个 TCP 连接,这称为多路复用。同一个请求和响应用一个流来表示,并有唯一的流 ID 来标识。

  多个请求和响应在 TCP 连接中可以乱序发送,到达目的地后再通过流 ID 重新组建。

   3.首部压缩

   HTTP2 提供了首部压缩功能。

  一般同一个网页的很多请求,头部数据都是重复的。如果可以把相同的首部存储起来,仅发送它们之间不同的部分,就可以节省不少的流量,加快请求的时间。

   HTTP/2 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送。

  当客户端发送请求时,它会根据首部值创建一张表:

  如果服务器收到了请求,它会照样创建一张表。

  当客户端发送下一个请求的时候,如果首部相同,它可以直接发送首部块:

  服务器会查找先前建立的表格,并把这些数字还原成索引对应的完整首部。

   4.优先级

   HTTP2 可以对比较紧急的请求设置一个较高的优先级,服务器在收到这样的请求后,可以优先处理。

   5.流量控制

  由于一个 TCP 连接流量带宽(根据客户端到服务器的网络带宽而定)是固定的,当有多个请求并发时,一个请求占的流量多,另一个请求占的流量就会少。流量控制可以对不同的流的流量进行精确控制。

   6.服务器推送

   HTTP2 新增的一个强大的新功能,就是服务器可以对一个客户端请求发送多个响应。换句话说,除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。

  例如当浏览器请求一个网站时,除了返回 HTML 页面外,服务器还可以根据 HTML 页面中的资源的 URL ,来提前推送资源。

2.3 使用服务端渲染

  客户端渲染: 获取 HTML 文件,根据需要下载 JavaScript 文件,运行文件,生成 DOM ,再渲染。

  服务端渲染:服务端返回 HTML 文件,客户端只需解析 HTML

  • 优点:首屏渲染快, SEO 好。
  • 缺点:配置麻烦,增加了服务器的计算压力。

  一般网站都是首页做服务端渲染,这样可以被爬虫更方便的检索到。

  服务端渲染的好处就是,页面是已经完成加载的状态,直接返回给浏览器就可以进行渲染了,不需要再对图片之类的其他资源再进行请求了。

2.4 静态资源使用 CDN

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

   CDN 原理

  当用户访问一个网站时,如果没有 CDN ,过程是这样的:

  1. 浏览器要将域名解析为 IP 地址,所以需要向本地 DNS 发出请求。
  2. 本地 DNS 依次向根服务器、顶级域名服务器、二级名称服务器、权限服务器发出请求,得到网站服务器的 IP 地址。
  3. 本地 DNS IP 地址发回给浏览器,浏览器向网站服务器 IP 地址发出请求并得到资源。

在这里插入图片描述

  如果用户访问的网站部署了 CDN ,过程是这样的:

  1. 浏览器要将域名解析为 IP 地址,所以需要向本地 DNS 发出请求。
  2. 本地 DNS 依次向根服务器、顶级域名服务器、权限服务器发出请求,得到全局负载均衡系统( GSLB )的 IP 地址。
  3. 本地 DNS 再向 GSLB 发出请求, GSLB 的主要功能是根据本地 DNS IP 地址判断用户的位置,筛选出距离用户较近的本地负载均衡系统( SLB ),并将该 SLB IP 地址作为结果返回给本地 。
  4. 本地 DNS SLB IP 地址发回给浏览器,浏览器向 SLB 发出请求。
  5. SLB 根据浏览器请求的资源和地址,选出最优的缓存服务器发回给浏览器。
  6. 浏览器再根据 SLB 发回的地址重定向到缓存服务器。
  7. 如果缓存服务器有浏览器需要的资源,就将资源发回给浏览器。如果没有,就向源服务器请求资源,再发给浏览器并缓存在本地。
    在这里插入图片描述

2.5 将 CSS 放在文件头部,JavaScript 文件放在底部

  所有放在 head 标签里的 CSS JS 文件都会堵塞渲染。如果这些 CSS JS 需要加载和解析很久的话,那么页面就空白了。所以 JS 文件要放在底部,等 HTML 解析完了再加载 JS 文件。

  那为什么 CSS 文件还要放在头部呢?

  因为先加载 HTML 再加载 CSS ,会让用户第一时间看到的页面是没有样式的、“丑陋”的,为了避免这种情况发生,就要将 CSS 文件放在头部了。

  另外, JS 文件也不是不可以放在头部,只要给 script 标签加上 defer 属性就可以了,异步下载,延迟执行。

2.6 使用字体图标 iconfont 代替图片图标

  字体图标就是将图标制作成一个字体,使用时就跟字体一样,可以设置属性,例如 font-size、color 等等,非常方便。并且字体图标是矢量图,不会失真。还有一个优点是生成的文件特别小。

  压缩字体文件

  使用 fontmin-webpack 插件对字体文件进行压缩

2.7 善用缓存,不重复加载相同的资源

  为了避免用户每次访问网站都得请求文件,我们可以通过添加 Expires max-age 来控制这一行为。

   Expires 设置了一个时间,只要在这个时间之前,浏览器都不会请求文件,而是直接使用缓存。而 max-age 是一个相对时间,建议使用 max-age 代替 Expires

  不过这样会产生一个问题,当文件更新了怎么办?怎么通知浏览器重新请求文件?

  可以通过更新页面中引用的资源链接地址,让浏览器主动放弃缓存,加载新资源。

  具体做法是把资源地址 URL 的修改与文件内容关联起来,也就是说,只有文件内容变化,才会导致相应 URL 的变更,从而实现文件级别的精确缓存控制。什么东西与文件内容相关呢?我们会很自然的联想到利用“数据摘要算法”对文件求摘要信息,摘要信息与文件内容一一对应,就有了一种可以精确到单个文件粒度的缓存控制依据了。

2.8 压缩文件

  压缩文件可以减少文件下载时间,让用户体验性更好。

  得益于 webpack node 的发展,现在压缩文件已经非常方便了。

  在 webpack 可以使用如下插件进行压缩:

JavaScript:UglifyPlugin
CSS :MiniCssExtractPlugin
HTML:HtmlWebpackPlugin

  其实,我们还可以做得更好。那就是使用 gzip 压缩。可以通过向 HTTP 请求头中的 Accept-Encoding 头添加 gzip 标识来开启这一功能。当然,服务器也得支持这一功能。

  附上 webpack node 配置 gzip 的使用方法。

  下载插件

npm install compression-webpack-plugin --save-dev
npm install compression

   webpack 配置

const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
    
    
  plugins: [new CompressionPlugin()],
}

   node 配置

const compression = require('compression')
// 在其他中间件前使用
app.use(compression())

2.9 图片优化

   图片延迟加载

  在页面中,先不给图片设置路径,只有当图片出现在浏览器的可视区域时,才去加载真正的图片,这就是延迟加载。对于图片很多的网站来说,一次性加载全部图片,会对用户体验造成很大的影响,所以需要使用图片延迟加载。

  首先可以将图片这样设置,在页面不可见时图片不会加载:

<img data-src="https://avatars0.githubusercontent.com/u/22117876">

  等页面可见时,使用 JS 加载图片:

const img = document.querySelector('img')
img.src = img.dataset.src

  这样图片就加载出来了,完整的代码可以看一下参考资料。

   响应式图片

  响应式图片的优点是浏览器能够根据屏幕大小自动加载合适的图片。

  通过 picture 实现

<picture>
    <source srcset="banner_w1000.jpg" media="(min-width: 801px)">
    <source srcset="banner_w800.jpg" media="(max-width: 800px)">
    <img src="banner_w800.jpg" alt="">
</picture>

  通过 @media 实现

@media (min-width: 769px) {
    
    
    .bg {
    
    
        background-image: url(bg1080.jpg);
    }
}
@media (max-width: 768px) {
    
    
    .bg {
    
    
        background-image: url(bg768.jpg);
    }
}

   调整图片大小

  例如,你有一个 1920 * 1080 大小的图片,用缩略图的方式展示给用户,并且当用户鼠标悬停在上面时才展示全图。如果用户从未真正将鼠标悬停在缩略图上,则浪费了下载图片的时间。

  所以,我们可以用两张图片来实行优化。一开始,只加载缩略图,当用户悬停在图片上时,才加载大图。还有一种办法,即对大图进行延迟加载,在所有元素都加载完成后手动更改大图的 src 进行下载。

   降低图片质量

  例如 JPG 格式的图片,100% 的质量和 90% 质量的通常看不出来区别,尤其是用来当背景图的时候。用 PS 切背景图时, 将图片切成 JPG 格式,并且将它压缩到 60% 的质量,基本上看不出来区别。

  压缩方法有两种,一是通过 webpack 插件 image-webpack-loader ,二是通过在线网站进行压缩。

  以下附上 webpack 插件 image-webpack-loader 的用法。

   webpack 配置

{
    
    
  test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
  use:[
    {
    
    
    loader: 'url-loader',
    options: {
    
    
      limit: 10000, /* 图片大小小于1000字节限制时会自动转成 base64 码引用*/
      name: utils.assetsPath('img/[name].[hash:7].[ext]')
      }
    },
    /*对图片进行压缩*/
    {
    
    
      loader: 'image-webpack-loader',
      options: {
    
    
        bypassOnDebug: true,
      }
    }
  ]
}

   尽可能利用 CSS3 效果代替图片

  有很多图片使用 CSS 效果(渐变、阴影等)就能画出来,这种情况选择 CSS3 效果更好。因为代码大小通常是图片大小的几分之一甚至几十分之一。

   使用 webp 格式的图片

   WebP 的优势体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;同时具备了无损和有损的压缩模式、 Alpha 透明以及动画的特性,在 JPEG PNG 上的转化效果都相当优秀、稳定和统一。

2.10 Webpack 对于 JS 的相关优化

   缓存(第二次访问速度更块)

  1. hash :文件打包每次都生成一个唯一哈希值,不管文件变不变。
  2. chunkhash :如果打包来自同一个入口,那么就属于同一个 chunk ,共享一个哈希值。
  3. contenthash :根据文件内容声称哈希值,文件内容不同,哈希值一定不同。

   tree shaking

  去除业务程序中没有使用的代码,让代码体积更小。(必须开启 es6 模块,生产环境会自动开启)

   code split

  单入口: js 文件代码会被加载成一个文件,体积过大,可以吧一个 bundle 拆分成多个,并行加载,提高速度。

  多入口:有几个入口输出几个 bundle 。增加 optimization 后不会重复打包相同文件。

   dll :对第三方库进行细化打包,生成多个文件。

   懒加载 / 预加载

   js 懒加载,用到才加载,不用不加载,异步中执行代码分隔。

   js 预加载:其他资源加载完,再加载(兼容性有待提高)。

三.提高页面运行速度

3.1 减少回流和重绘

  浏览器渲染过程

  1. 解析 HTML 生成 DOM 树。
  2. 解析 CSS 生成CSSOM 规则树。
  3. DOM 树与 CSSOM 规则树合并在一起生成渲染树。
  4. 遍历渲染树开始布局,计算每个节点的位置大小信息。
  5. 将渲染树每个节点绘制到屏幕

   回流

  当改变 DOM 元素位置或大小时,会导致浏览器重新生成渲染树,这个过程叫回流。

   重绘

  当重新生成渲染树后,就要将渲染树每个节点绘制到屏幕,这个过程叫重绘。不是所有的动作都会导致回流,例如改变字体颜色,只会导致重绘。记住,回流会导致重绘,重绘不会导致回流。

  回流和重绘这两个操作都是非常昂贵的,因为 JavaScript 引擎线程与 GUI 渲染线程是互斥,它们同时只能一个在工作。

   什么操作会导致回流?

  • 添加或删除可见的 DOM 元素
  • 元素位置改变
  • 元素尺寸改变
  • 内容改变
  • 浏览器窗口尺寸改变

   如何减少回流重绘?

  • JavaScript 修改样式时,最好不要直接写样式,而是替换 class 来改变样式。
  • 如果要对 DOM 元素执行一系列操作,可以将 DOM 元素脱离文档流,修改完成后,再将它带回文档。推荐使用隐藏元素( display:none )或文档碎片( DocumentFragement ),都能很好的实现这个方案。

3.2 使用事件委托

  事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。所有用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术, 使用事件委托可以节省内存。

3.3 节约内存

  继承的运用:可以节约一大部分内存, CommonJS 使用寄生组合继承, ES6 使用类继承。

  垃圾回收:对于不用的资源,使其指针指向 null 释放其内存。

3.4 Flex代替传统布局

  这方面兼容性上需要注意。

四、总结

  对于提高网页性能这个问题来说,包含的面非常广,几乎所有的新技术,目的都是为了提高网页性能,这个课题也是所有工程师共同追求的目标,所以当我们接触到这方面的问题时,需要涵盖各个方面,所以需要各位在平时工作学习中,注意积累,任何好的方法都可以记录下来,作为我们日后优化的手段。

  当然所有的优化方法都有针对的情况,优化需要因地制宜,并不是把所有的优化手段都用上才是最好的,适合才好。




猜你喜欢

转载自blog.csdn.net/EcbJS/article/details/114664102