前端性能优化简介

322 阅读13分钟

一、关于性能优化

我们知道,现在就是一个“流量为王”的时代,一个网站最重要的的就是用户,有了用户你才能有业务,打比方,你是一个电商网站,那么你肯定希望你的用户越多越好,这样才会有更多的人去浏览你的商品,从而在你的网站上花钱,买东西,这样你才能产生收益,但假如你的网站打开要十几秒,请求接口要十几秒,那用户还愿意等么?

看一下以下的用户体验图:

二、网页性能指标及影响因素

1、从输入 url 到页面展示中间发生了什么?

PerformanceNavigationTiming的执行顺序图(Navigation Timing Level2模型)

页面运行的时间线(统计了从浏览器从网址开始导航到 window.onload事件触发的一系列关键的时间点):

(更详细标准的解释请参看:W3C Editor's Draft)

  1. 输入网址——告诉浏览器你要去哪里
  2. 浏览器查找 DNS——网络世界是 IP 地址的世界,DNS 就是 ip 地址的别名。从本地 DNS 到最顶级 DNS 一步一步的往上爬,直到命中需要访问的 IP 地址。(最中会指向13根)
  • DNS 预解析——使用 CDN 缓存,加快解析 CDN 寻找目标地址(dns-prefetch)
  1. 客户端和服务器建立连接——建立 TCP 的安全通道,3次握手
  • CDN 加速——适用内容分发网络,让用户更快的获取到索要的内容;
  • 启用压缩——在 http 协议中,使用类似 gzip 压缩的方案(对服务器资源不足的时候进行权衡);
  • 适用 HTTP 2.0协议——http2.0优化了很多东西,包括首部hpack 算法压缩、多路复用、server push等;
  1. 浏览器发送 http 请求——默认长连接(复用同一个 TCP 通道,短链接:每次链接完就销毁)
  • 减少 http 请求——每个请求从创建到销毁都会消耗很多资源和时间,减少请求就可以相对来说更快展示内容;
    • 压缩合并 js 文件以及 css 文件
    • 针对图片,可以将图片进行合并然后下载,通过 css Sprites 切割展示()控制大小,太大的话反而适得其反)
  • 使用 http 缓存——缓存原则:越多越好,越久越好。让客户端发送更少请求,直接本地获取,加快性能;
  • 减少 cookie 请求——针对非必要数据(静态资源)请求,进行跨域隔离,减少传输内容大小;
  • 预加载请求 —— 针对一些业务中场景可预加载的内容,提前加载,在之后的用户操作中更少的请求,更快的响应。
  • 选择 get 和 post——在 http 定义的时候,get 本质上就是获取数据,post 是发送数据。get 可以在一个TCP 报文完成请求,但是 post 先发 header,再发送数据,所以,请考虑好请求选型;
  • 缓存方案选型——递进式缓存更新(防止一次性丢失大量缓存,导致负载骤多);
  1. 服务器响应请求——tomcat、IIS 等服务器通过本地映射文件关系找到地址或者通过数据库查找到数据,处理完成返回给浏览器;
  • 后端框架选型——更快的响应,前端更快的操作;
  • 数据库选型和优化——更快的响应,前端更快的操作;
  1. 浏览器接受响应——浏览器根据报文头里面的数据进行不同的响应处理
  • 解耦第三方依赖——越多的第三方的不确定因素,会导致 web 的不稳定性和不确定性;
  • 避免404资源——请求资源不到浪费了从请求到接受的所有资源
  1. 浏览器渲染顺序
  • HTML解析开始构建 dom 树;

  • 外部脚本和样式表加载完毕;

    • 尽快加载 css,首先将 CSSOM对象渲染出来,然后进行页面渲染,否则导致页面闪屏,用户体验差
    • css 选择器是从右往左解析的,类似于#test a {color: #000},css解析器会查找所有 a 标签的祖先节点,所以效率不是那么高;
    • 在css 媒介查询中,最好不要直接和任何 css 规则相关。最好写到 link 标签中,告诉浏览器,只有在这个媒介下,加载指定这个 css;
  • 脚本在文档内解析并执行;

    • 按需加载脚本,例如现在的 webpack 就可以打包和按需加载 js 脚本;
    • 将脚本标记为异步,不阻塞页面渲染,最佳启动,保证无关主要的脚本不会阻塞页面;
    • 慎重选择框架和类库,避免只是用类库和框架的一个功能和函数,从而引入了整个文件;

2、关于Performance API

  1. performance.timing可以获取网页运行过程中每个时间点对应的时间戳(绝对时间,ms),但却即将**「废弃」**

  1. 取而代之window.performance.getEntriesByType('navigation')[0]

3、常用的计算性能指标

网页重定向的耗时:redirectEnd - redirectStart

检查本地缓存的耗时: domainLookupStart - fetchStart

DNS查询的耗时:domainLookupEnd - domainLookupStart

TCP连接的耗时:connectEnd - connectStart

从客户端发起请求到接收到响应的时间 / TTFB:responseStart - fetchStart

首次渲染时间/白屏时间:responseStart - pnt.startTime

下载服务端返回数据的时间:responseEnd - responseStart

request请求耗时:responseEnd - requestStart

解析dom树耗时:domComplete - domInteractive

dom加载完成的时间:domContentLoadedEventEnd

页面load的总耗时:duration

3、常见的首屏性能指标定义

  • FP(First Paint),代表浏览器第一次在页面上绘制的时间,这个时间仅仅是开始绘制的时间,但是未必真的绘制了什么有效的内容。
  • FCP(First Contentful Paint),代表浏览器第一次绘制出 DOM 元素(如文字、标签等)的时间。FP 可能和 FCP 是同一个时间,也可能早于 FCP,但一般来说两者的差距不会太大。
  • FMP(First Meaningful Paint),首次渲染有意义的内容的时间,“有意义”没有一个标准的定义,FMP的计算方法也很复杂。
  • LCP(largest contentful Paint),度量的是首屏视图中最大的元素的渲染时间。
  • FID(largest contentful Paint),度量的是从用户首次和网站进行交互到响应该事件的实际延时的时间。
  • CLS(largest contentful Paint),CLS 度量的是页面产生连续累计布局偏移分数。
  • TTFB(Time To First Byte),是指客户端从发起请求到接收到服务器响应的第一个字节的时间,是反应网站性能的重要指标。

三、前端缓存

1、前端常见的缓存有哪些?

  1. dns 缓存
  2. cdn 缓存
  3. http 缓存
  4. 浏览器缓存
  5. 离线缓存
  6. nginx 缓存

...

2、http缓存

该阶段存在真正发起网络请求之前,浏览器会先在浏览器缓存中查询是否有要请求的文件。其中,浏览器缓存是一种在本地保存资源副本,以供下次请求时直接使用的技术。如果浏览器缓存中存有副本,它会拦截请求,返回该资源的副本,并直接结束请求,而不会再去源服务器重新下载。

强制缓存:

1、 Expires :

http1.0 增加

字段的作用是,设定一个强缓存时间(单位为秒)。在此时间范围内,则从内存(memory cache)或硬盘(disk cache)中读取缓存并返回。

2、Cache-Control:max-age=3600 (单位秒)

Cache-Control这个字段在http1.1中被增加,Cache-Control完美解决了Expires本地时间和服务器时间不同步的问题。

Cache-Control有max-age、s-maxage、no-cache、no-store、private、public这六个属性。

  • max-age: 决定客户端资源被缓存多久。
  • s-maxage: 决定代理服务器缓存的时长。
  • no-cache: 表示是强制进行协商缓存。
  • no-store: 是表示禁止任何缓存策略。
  • public: 表示资源即可以被浏览器缓存也可以被代理服务器缓存。
  • private: 表示资源只能被浏览器缓存。

协商缓存:

last-modified:文件最后一次改变时间(秒),

If-Modified-Since:当客户端读取到last-modified的时候,会在下次的请求标头中携带一个字段:If-Modified-Since。

Eag:文件指纹,根据文件内容计算出的唯一哈希值。文件内容一旦改变则指纹改变。

if-None-Match: 客户端自动从缓存中读取出上一次服务端返回的Etag 也就是文件指纹。并赋给请求头的if-None-Match字段,让上一次的文件指纹跟随请求一起回到服务端。

强缓存和协商缓存的区别

  • 优先查找强缓存,没有命中再查找协商缓存
  • 强缓存状态码是200,协商缓存是304
  • 强缓存不发请求到服务器,所以有时候资源更新了浏览器还不知道,但是协商缓存会发请求到服务器,资源是否有更新,服务器肯定知道
  • 大部分web服务器都默认开启协商缓存。
  • 目前项目大多数使用缓存方案 :
  1. 协商缓存一般存储:html
  2. 强缓存一般存储:css, image, js,文件名带上 hash

webpack 常用的三种 hash

  • hash:跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值。
  • chunkhash :不同的入口文件进行依赖文件解析、构建对应的 chunk ,生成对应的哈希值,文件本身修改或者依赖文件修改, chunkhash 值会变化。(js 适用)
  • contenthash:每个文件自己单独的 hash 值,文件的改动只会影响自身的 hash 值。(css和图片等适用)

四、常见的首屏优化方案及原理

1、异步加载

说起**「异步加载」**,我们需要先了解一下什么是同步加载?

// 默认就是同步加载
<script src="http://abc.com/script.js"></script>
  • 同步加载: 同步模式又称**「阻塞模式」**,会阻止浏览器的后续处理,停止了后续的文件的解析,执行,如图像的渲染。流览器之所以会采用同步模式,是因为加载的js文件中有对dom的操作,重定向,输出document等默认行为,所以同步才是最安全的。所以一般我们都会把script标签放置在body结束标签之前,减少阻塞。
  • 所以异步加载,其实就是一种**「非阻塞加载模式」**的方式,就是浏览器在下载执行js的同时,还会继续进行后续页面的处理。

几种常见的异步加载脚本方式:

  1. async和defer

在JavaScript脚本增加async或者defer属性

// 问: script标签的defer和async的区别?

// defer要等到html解析完成之后执行脚本
<script src="main.js" defer></script>
// async异步加载脚本后便会执行脚本
<script src="main.js" async></script>
  1. 动态添加 script 标签
// js代码中动态添加script标签,并将其插入页面
const script = document.createElement("script");
script.src = "a.js"; 
document.head.appendChild(script);

2、代码分割

  • 安装analyze插件分析项目依赖包的体积大小
  • 以 3.0 项目为例
  • webpack-optimization-splitChunks
chunks: ['vendors','lodash','antv','antd','antDesign','phoenixMapWorksPlugin', 'umi'],
  chainWebpack: function (config, { webpack }) {
  // if (UMI_ENV !== 'dev') {
  config.merge({
    optimization: {
      splitChunks: {
        chunks: 'initial',
        minSize: 500000,
        automaticNameDelimiter: '.',
        cacheGroups: {
          phoenixMapWorksPlugin: {
            name: 'phoenixMapWorksPlugin',
            test: /[\/]node_modules[\/](phoenix-map-works-plugin)[\/]/,
            priority: 10,
            chunks: 'all',
          },
          antDesign: {
            name: 'antDesign',
            test: /[\/]node_modules[\/](@ant-design)[\/]/,
            priority: 9,
            chunks: 'all',
          },
          antd: {
            name: 'antd',
            test: /[\/]node_modules[\/](antd)[\/]/,
            priority: 8,
            chunks: 'all',
          },
          antv: {
            name: 'antv',
            test: /[\/]node_modules[\/](@antv)[\/]/,
            priority: 7,
            chunks: 'all',
          },
          lodash: {
            name: 'lodash',
            test: /[\/]node_modules[\/](lodash)[\/]/,
            priority: 6,
            chunks: 'all',
            enforce: true,
          },
          vendor: {
            name: 'vendors',
            test({ resource }: any) {
              return /[\/]node_modules[\/]/.test(resource);
            },
            priority: 1,
            chunks: 'initial',
            enforce: true,
          },
        },
      },
    },
  });
  // }
},
  • 项目是 http1.1,由于 chrome 在 http1.1 的情况下同一域名最大并发请求 6 个,所以合理的拆包会更快(考虑一个10M 的资源加载10s ,和拆成5个 2M 的资源,并行加载2s )

3、按需加载

  • 路由组件的按需加载
// umi config 配置
dynamicImport: {
    loading: '@/components/PageLoading',
  },
  • 非首屏资源的动态导入
const MyComponent = React.lazy(() => import('./MyComponent'));

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <MyComponent />
      </Suspense>
    </div>
  );
}
  • 按需加载是把本应该在bundle里的部分 单独拿出来, 放到顶层的promise队列里面 ,用函数异步import 达到按需的效果。所以 按需的chunk并没有合并到bundle里 。

4、代码压缩

  • 开启 gzip 压缩
  • compression-webpack-plugin
 config.plugin('compression-webpack-plugin').use(
      new CompressionPlugin({
        test: /.(js|css|html)$/i, // 匹配
        threshold: 10240, // 超过10k的文件压缩
        deleteOriginalAssets: false, // 不删除源文件
        algorithm: "gzip", //使用gzip压缩
        minRatio: 0.8 //压缩率小于0.8才会压缩
      }),
    );
  • nginx 压缩

http{
  # 开启压缩机制
  gzip on;
  # 指定会被压缩的文件类型(也可自己配置其他类型)
  gzip_types text/plain application/javascript text/css application/xml text/javascript image/jpeg image/gif image/png;
  # 设置压缩级别,越高资源消耗越大,但压缩效果越好
  gzip_comp_level 5;
  # 在头部中添加Vary: Accept-Encoding(建议开启)
  gzip_vary on;
  # 处理压缩请求的缓冲区数量和大小
  gzip_buffers 16 8k;
  # 对于不支持压缩功能的客户端请求不开启压缩机制
  gzip_disable "MSIE [1-6]."; # 低版本的IE浏览器不支持压缩
  # 设置压缩响应所支持的HTTP最低版本
  gzip_http_version 1.1;
  # 设置触发压缩的最小阈值
  gzip_min_length 2k;
  # 关闭对后端服务器的响应结果进行压缩
  gzip_proxied off;
}
  • gz 压缩可以使代码体积降低 70% 左右,但是浏览器解压也会需要一定的时间,所以应该结合具体场景选择性的采取压缩。
  • 考虑一个 js 文件 10M,加载耗时 10s。压缩后 3M ,压缩后加载耗时 3s,解压用时 0.5s,总体耗时对比就是 10s :3.5s

5、效果展示

  • 3.0 项目为例,仅使用 nginx 压缩

资源加载完毕耗时:3.5s

  • nginx 压缩 + 代码分割 + 按需加载

资源加载完毕耗时:2.11s

6、其它首屏优化方案

  1. http1.1 => http2
  2. 静态资源丢 CDN
  3. 某些首屏资源文件 preload ,prefetch
  4. 核心依赖库(版本基本不会变的)使用externals外部依赖
  5. BFF
  6. SSR(更利于SEO)

.....

五、其它非首屏优化

运行时优化

1、react 代码优化

2、vue 代码优化

3、常见代码执行效率优化

4、图片优化

5、长列表、懒加载、异步加载、web worker等

构建时优化

1、不同构建工具之间的对比

2、冷启动

3、热更新

4、build

5、CI/CD阶段优化

webpack 为主导的项目可以做哪些优化?

开发环境:

  1. 开启 HMR 热模块替换
  2. 开启 source-map
  3. 使用 oneof

rules中匹配规则,命中一个就不继续往下走了。不需要挨个匹配。

  1. 开启 babel 缓存

babel-loader 中 options cacheDirectory 设置为 true。

开启 babel 缓存,第二次构建时会读取之前的缓存

  1. 使用 contenthash?使用合理的 hash

js 文件使用 chunkhash:

css 文件使用 contenthash:contenthash 将根据资源内容创建出唯一 hash,也就是说文件内容不变,hash 就不变。

  1. 开启多进程打包

js/tsx 的 rules 里配置,loader:"thread-loader"

要放在babel-loader 前面,进程开启需要600ms 时间。

  1. externals 引入外部包

使用 cdn 地址

生产环境:

  1. code split 代码分割

optimization:splitchunk

  1. 懒加载/预加载

import 的时候使用魔法注释

  1. 适用 tree-shaking

ems 即可

  1. 使用 pwa

work-webpack-plugin

生成 servicework 配置文件,实现离线缓存,断网访问