性能优化之笔记

性能优化分为网络优化和渲染优化

从输入 URL 到页面加载完成,发生了什么?
首先咱们须要经过 DNS(域名解析系统)将 URL 解析为对应的 IP 地址,而后与这个 IP 地址肯定的那台服务器创建起 TCP 网络链接,随后咱们向服务端抛出咱们的 HTTP 请求,服务端处理完咱们的请求以后,把目标数据放在 HTTP 响应里返回给客户端,拿到响应数据的浏览器就能够开始走一个渲染的流程。渲染完毕,页面便呈现给了用户,并时刻等待响应用户的操做
各个优化
DNS 解析花时间,能不能尽可能减小解析次数或者把解析前置?能——浏览器 DNS 缓存和 DNS prefetch
TCP 每次的三次握手都急死人,有没有解决方案?有——长链接、预链接、接入 SPDY 协议
这两个过程的优化每每须要咱们和团队的服务端工程师协做完成,
HTTP 请求 减小请求次数和减少请求体积方面css

浏览器端的性能优化——这部分涉及资源加载优化、服务端渲染、浏览器缓存机制的利用、DOM 树的构建、网页排版和渲染过程、回流与重绘的考量、DOM 操做的合理规避等等html

先说网络优化

从输入 URL 到显示页面这个过程当中,涉及到网络层面的,有三个主要过程:前端

  • DNS 解析
  • TCP 链接
  • HTTP 请求/响应

对于 DNS 解析和 TCP 链接两个步骤,咱们前端能够作的努力很是有限。相比之下,HTTP 链接这一层面的优化才是咱们网络优化的核心
HTTP 优化有两个大的方向:vue

  • 减小请求次数
  • 减小单次请求所花费的时间

指向了咱们平常开发中很是常见的操做——资源的压缩与合并
这就是咱们用构建工具在作的事情node

webpack 的性能瓶颈

webpack 的优化瓶颈,主要是两个方面:react

  • webpack 的构建过程太花时间
  • webpack 打包的结果体积太大

webpack 优化方案

构建过程提速策略

不要让 loader 作太多事情——以 babbabel-loader 无疑是强大的,但它也是慢的。

babel-loader 无疑是强大的,但它也是慢的。webpack

最多见的优化方式是,用 include 或 exclude 来帮咱们避免没必要要的转译,好比 webpack 官方在介绍 babel-loader 时给出的示例ios

module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    }
  ]
}

这段代码帮咱们规避了对庞大的 node_modules 文件夹或者 bower_components 文件夹的处理。但经过限定文件范围带来的性能提高是有限的。除此以外,若是咱们选择开启缓存将转译结果缓存至文件系统,则至少能够将 babel-loader 的工做效率提高两倍。要作到这点,咱们只须要为 loader 增长相应的参数设定:git

loader: 'babel-loader?cacheDirectory=true'

这个规则仅做用于这个 loader,像一些相似 UglifyJsPlugin 的 webpack 插件在工做时依然会被这些庞大的第三方库拖累,webpack 构建速度依然会所以大打折扣。程序员

第三方库的处理

处理第三方库的姿式有不少,其中,Externals 不够聪明,一些状况下会引起重复打包的问题;而 CommonsChunkPlugin 每次构建时都会从新构建一次 vendor;出于对效率的考虑,咱们这里为你们推荐 DllPlugin。
DllPlugin 是基于 Windows 动态连接库(dll)的思想被创做出来的。这个插件会把第三方库单独打包到一个文件中,这个文件就是一个单纯的依赖库。这个依赖库不会跟着你的业务代码一块儿被从新打包,只有当依赖自身发生版本变化时才会从新打包

用 DllPlugin 处理文件,要分两步走:

  • 基于 dll 专属的配置文件,打包 dll 库
  • 基于 webpack.config.js 文件,打包业务代码
    以一个基于 React 的简单项目为例,咱们的 dll 的配置文件能够编写以下:
const path = require('path')
const webpack = require('webpack')

module.exports = {
    entry: {
      // 依赖的库数组
      vendor: [
        'prop-types',
        'babel-polyfill',
        'react',
        'react-dom',
        'react-router-dom',
      ]
    },
    output: {
      path: path.join(__dirname, 'dist'),
      filename: '[name].js',
      library: '[name]_[hash]',
    },
    plugins: [
      new webpack.DllPlugin({
        // DllPlugin的name属性须要和libary保持一致
        name: '[name]_[hash]',
        path: path.join(__dirname, 'dist', '[name]-manifest.json'),
        // context须要和webpack.config.js保持一致
        context: __dirname,
      }),
    ],
}

编写完成以后,运行这个配置文件,咱们的 dist 文件夹里会出现这样两个文件:

vendor-manifest.json
vendor.js

vendor.js 没必要解释,是咱们第三方库打包的结果。这个多出来的 vendor-manifest.json,则用于描述每一个第三方库对应的具体路径,我这里截取一部分给你们看下:

{
  "name": "vendor_397f9e25e49947b8675d",
  "content": {
    "./node_modules/core-js/modules/_export.js": {
      "id": 0,
        "buildMeta": {
        "providedExports": true
      }
    },
    "./node_modules/prop-types/index.js": {
      "id": 1,
        "buildMeta": {
        "providedExports": true
      }
    },
    ...
  }
}

随后,咱们只需在 webpack.config.js 里针对 dll 稍做配置:

const path = require('path');
const webpack = require('webpack')
module.exports = {
  mode: 'production',
  // 编译入口
  entry: {
    main: './src/index.js'
  },
  // 目标文件
  output: {
    path: path.join(__dirname, 'dist/'),
    filename: '[name].js'
  },
  // dll相关配置
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      // manifest就是咱们第一步中打包出来的json文件
      manifest: require('./dist/vendor-manifest.json'),
    })
  ]
}

以上也可用有些繁琐也可用AutoDllPlugin替代

npm install --save-dev autodll-webpack-plugin

使用

const AutoDllPlugin = require('autodll-webpack-plugin');

plugins: [
    new AutoDllPlugin({
            inject: true, // will inject the DLL bundles to html
            context: path.join(__dirname, '..'),
            filename: '[name]_[hash].dll.js',
            path: 'res/js',
            plugins: mode === 'online' ? [
                new UglifyJsPlugin({
                    uglifyOptions: {
                        compress: {
                            warnings: false
                        }
                    },
                    sourceMap: config.build.productionSourceMap,
                    parallel: true
                })
            ] : [],
            entry: {
                vendor: ['vue/dist/vue.esm.js', 'vuex', 'axios', 'vue-router', 'babel-polyfill', 'lodash']
            }
     })
]

一次基于 dll 的 webpack 构建过程优化,便大功告成了!

Happypack——将 loader 由单进程转为多进程

你们知道,webpack 是单线程的,就算此刻存在多个任务,你也只能排队一个接一个地等待处理。这是 webpack 的缺点,好在咱们的 CPU 是多核的,Happypack 会充分释放 CPU 在多核并发方面的优点,帮咱们把任务分解给多个子进程去并发执行,大大提高打包效率。

HappyPack 的使用方法也很是简单,只须要咱们把对 loader 的配置转移到 HappyPack 中去就好,咱们能够手动告诉 HappyPack 咱们须要多少个并发的进程

const HappyPack = require('happypack')
// 手动建立进程池
const happyThreadPool =  HappyPack.ThreadPool({ size: os.cpus().length })

module.exports = {
  module: {
    rules: [
      ...
      {
        test: /\.js$/,
        // 问号后面的查询参数指定了处理这类文件的HappyPack实例的名字
        loader: 'happypack/loader?id=happyBabel',
        ...
      },
    ],
  },
  plugins: [
    ...
    new HappyPack({
      // 这个HappyPack的“名字”就叫作happyBabel,和楼上的查询参数遥相呼应
      id: 'happyBabel',
      // 指定进程池
      threadPool: happyThreadPool,
      loaders: ['babel-loader?cacheDirectory']
    })
  ],
}

构建结果体积压缩

文件结构可视化,找出致使体积过大的缘由

这里为你们介绍一个很是好用的包组成可视化工具——webpack-bundle-analyzer,配置方法和普通的 plugin 无异,它会以矩形树图的形式将包内各个模块的大小和依赖关系呈现出来,格局如官方所提供这张图所示:

!------)
image.png
在使用时,咱们只须要将其以插件的形式引入

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

拆分资源

这点仍然围绕 DllPlugin 展开

删除冗余代码

一个比较典型的应用,就是 Tree-Shaking
基于 import/export 语法,Tree-Shaking 能够在编译的过程当中获悉哪些模块并无真正被使用,这些没用的代码,在最后打包的时候会被去除。
适合用来处理模块级别的冗余代码。至于粒度更细的冗余代码的去除,每每会被整合进 JS 或 CSS 的压缩或分离过程当中。
这里咱们以当下接受度较高的 UglifyJsPlugin 为例,看一下如何在压缩过程当中对碎片化的冗余代码(如 console 语句、注释等)进行自动化删除:

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
  plugins: [
    new UglifyJsPlugin({
      // 容许并发
      parallel: true,
      // 开启缓存
      cache: true,
      compress: {
        // 删除全部的console语句    
        drop_console: true,
        // 把使用屡次的静态值自动定义为变量
        reduce_vars: true,
      },
      output: {
        // 不保留注释
        comment: false,
        // 使输出的代码尽量紧凑
        beautify: false
      }
    })
  ]
}

webpack4 中,咱们是经过配置 optimization.minimize 与 optimization.minimizer 来自定义压缩相关的操做的。

按需加载

  • 一次不加载完全部的文件内容,只加载此刻须要用到的那部分(会提早作拆分)
  • 当须要更多内容时,再对用到的内容进行即时加载

当咱们不须要按需加载的时候,咱们的代码是这样的:

import BugComponent from '../pages/BugComponent'
...
<Route path="/bug" component={BugComponent}>

为了开启按需加载,咱们要稍做改动。
首先 webpack 的配置文件要走起来:

output: {
 path: path.join(__dirname, '/../dist'),
 filename: 'app.js',
 publicPath: defaultSettings.publicPath,
 // 指定 chunkFilename
 chunkFilename: '[name].[chunkhash:5].chunk.js',
},

路由处的代码也要作一下配合;

const getComponent => (location, cb) {
  require.ensure([], (require) => {
    cb(null, require('../pages/BugComponent').default)
  }, 'bug')
},

<Route path="/bug" getComponent={getComponent}>

核心就是这个方法:

require.ensure(dependencies, callback, chunkName)

这是一个异步的方法,webpack 在打包时,BugComponent 会被单独打成一个文件,只有在咱们跳转 bug 这个路由的时候,这个异步方法的回调才会生效,才会真正地去获取 BugComponent 的内容。这就是按需加载。

按需加载的粒度,还能够继续细化,细化到更小的组件、细化到某个功能点,都是 ok 的。

Gzip 压缩原理

开启 Gzip。
具体的作法很是简单,只须要你在你的 request headers 中加上这么一句:

accept-encoding:gzip

咱们前端关系更密切的话题:HTTP 压缩。

HTTP 压缩是一种内置到网页服务器和网页客户端中以改进传输速度和带宽利用率的方式。在使用 HTTP 压缩的状况下,HTTP 数据在从服务器发送前就已压缩:兼容的浏览器将在下载所需的格式前宣告支持何种方法给服务器;不支持压缩方法的浏览器将下载未经压缩的数据。最多见的压缩方案包括 Gzip 和 Deflate。

HTTP 压缩就是以缩小体积为目的,对 HTTP 内容进行从新编码的过程
Gzip 的内核就是 Deflate,目前咱们压缩文件用得最多的就是 Gzip。能够说,Gzip 就是 HTTP 压缩的经典例题。

该不应用 Gzip

压缩 Gzip,服务端要花时间;解压 Gzip,浏览器要花时间。中间节省出来的传输时间,真的那么可观吗?
咱们处理的都是具有必定规模的项目文件。实践证实,这种状况下压缩和解压带来的时间开销相对于传输过程当中节省下的时间开销来讲,能够说是微不足道的。

Gzip 是万能的吗

首先要认可 Gzip 是高效的,压缩后一般能帮咱们减小响应 70% 左右的大小。

但它并不是万能。Gzip 并不保证针对每个文件的压缩都会使其变小。

Gzip 压缩背后的原理,是在一个文本文件中找出一些重复出现的字符串、临时替换它们,从而使整个文件变小。根据这个原理,文件中代码的重复率越高,那么压缩的效率就越高,使用 Gzip 的收益也就越大。反之亦然。

webpack 的 Gzip 和服务端的 Gzip

通常来讲,Gzip 压缩是服务器的活儿:服务器了解到咱们这边有一个 Gzip 压缩的需求,它会启动本身的 CPU 去为咱们完成这个任务。而压缩文件这个过程自己是须要耗费时间的,你们能够理解为咱们以服务器压缩的时间开销和 CPU 开销(以及浏览器解析压缩文件的开销)为代价,省下了一些传输过程当中的时间开销。

既然存在着这样的交换,那么就要求咱们学会权衡。服务器的 CPU 性能不是无限的,若是存在大量的压缩需求,服务器也扛不住的。服务器一旦所以慢下来了,用户仍是要等。Webpack 中 Gzip 压缩操做的存在,事实上就是为了在构建过程当中去作一部分服务器的工做,为服务器分压。

所以,这两个地方的 Gzip 压缩,谁也不能替代谁。它们必须和平共处,好好合做。做为开发者,咱们也应该结合业务压力的实际强度状况,去作好这其中的权衡。

图片优化——质量与性能的博弈

图片是电商平台的重要资源,甚至有人说“作电商就是作图片”。

就图片这块来讲,与其说咱们是在作“优化”,不如说咱们是在作“权衡”。由于咱们要作的事情,就是去压缩图片的体积(或者一开始就选取体积较小的图片格式)。但这个优化操做,是以牺牲一部分红像质量为代价的。所以咱们的主要任务,是尽量地去寻求一个质量与性能之间的平衡点。
时下应用较为普遍的 Web 图片格式有 JPEG/JPG、PNG、WebP、Base6四、SVG 等
不谈业务场景的选型都是耍流氓

在计算机中,像素用二进制数来表示。不一样的图片格式中像素与二进制位数之间的对应关系是不一样的。一个像素对应的二进制位数越多,它能够表示的颜色种类就越多,成像效果也就越细腻,文件体积相应也会越大。
一个二进制位表示两种颜色(0|1 对应黑|白),若是一种图片格式对应的二进制位数有 n 个,那么它就能够呈现 2^n 种颜色。

JPEG/JPG

关键字:有损压缩、体积小、加载快、不支持透明

JPG 的优势

JPG 最大的特色是有损压缩
。这种高效的压缩算法使它成为了一种很是轻巧的图片格式。另外一方面,即便被称为“有损”压缩,JPG的压缩方式仍然是一种高质量的压缩方式:当咱们把图片体积压缩至原有体积的 50% 如下时,JPG 仍然能够保持住 60% 的品质。此外,JPG 格式以 24 位存储单个图,能够呈现多达 1600 万种颜色,足以应对大多数场景下对色彩的要求,这一点决定了它压缩先后的质量损耗并不容易被咱们人类的肉眼所察觉
JPG 适用于呈现色彩丰富的图片,在咱们平常开发中,JPG 图片常常做为大的背景图、轮播图或 Banner 图出现。

JPG 的缺陷

有损压缩在上文所展现的轮播图上确实很难露出马脚,但当它处理矢量图形Logo 等线条感较强、颜色对比强烈的图像时,人为压缩致使的图片模糊会至关明显。

此外,JPEG 图像不支持透明度处理,透明图片须要召唤 PNG 来呈现。

PNG-8 与 PNG-24

关键字:无损压缩、质量高、体积大、支持透明

PNG 的优势

PNG(可移植网络图形格式)是一种无损压缩的高保真的图片格式。8 和 24,这里都是二进制数的位数。按照咱们前置知识里提到的对应关系,8 位的 PNG 最多支持 256 种颜色,而 24 位的能够呈现约 1600 万种颜色。
PNG 图片具备比 JPG 更强的色彩表现力,对线条的处理更加细腻,对透明度有良好的支持。它弥补了上文咱们提到的 JPG 的局限性,惟一的 BUG 就是体积太大
前面咱们提到,复杂的、色彩层次丰富的图片,用 PNG 来处理的话,成本会比较高,咱们通常会交给 JPG 去存储。

考虑到 PNG 在处理线条和颜色对比度方面的优点,咱们主要用它来呈现小的 Logo、颜色简单且对比强烈的图片或背景等。

SVG

文本文件、体积小、不失真、兼容性好

SVG(可缩放矢量图形)是一种基于 XML 语法的图像格式。它和本文说起的其它图片种类有着本质的不一样:SVG 对图像的处理不是基于像素点,而是是基于对图像的形状描述。

和性能关系最密切的一点就是:SVG 与 PNG 和 JPG 相比,文件体积更小,可压缩性更强

固然,做为矢量图,它最显著的优点仍是在于图片可无限放大而不失真这一点上。这使得 SVG 即便是被放到视网膜屏幕上,也能够一如既往地展示出较好的成像品质——1 张 SVG 足以适配 n 种分辨率。

此外,SVG 是文本文件。咱们既能够像写代码同样定义 SVG,把它写在 HTML 里、成为 DOM 的一部分,也能够把对图形的描述写入以 .svg 为后缀的独立文件(SVG 文件在使用上与普通图片文件无异)。这使得 SVG 文件能够被很是多的工具读取和修改,具备较强的灵活性

SVG 的局限性主要有两个方面,一方面是它的渲染成本比较高,这点对性能来讲是很不利的。另外一方面,SVG 存在着其它图片格式所没有的学习成本(它是可编程的)

SVG 的使用方式与应用场景

SVG 是文本文件,咱们既能够像写代码同样定义 SVG,把它写在 HTML 里、成为 DOM 的一部分,也能够把对图形的描述写入以 .svg 为后缀的独立文件(SVG 文件在使用上与普通图片文件无异)。

  • 将 SVG 写入 HTML:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <svg xmlns="http://www.w3.org/2000/svg"   width="200" height="200">
        <circle cx="50" cy="50" r="50" />
    </svg>
</body>
</html>

将 SVG 写入独立文件后引入 HTML:

<img src="文件名.svg" alt="">

在实际开发中,咱们更多用到的是后者。不少状况下设计师会给到咱们 SVG 文件,就算没有设计师,咱们还有很是好用的 在线矢量图形库。对于矢量图,咱们无须深究过多,只须要对其核心特性有所掌握、往后在应用时作到有迹可循便可。

最经典的小图标解决方案——雪碧图(CSS Sprites)

一种将小图标和背景图像合并到一张图片上,而后利用 CSS 的背景定位来显示其中的每一部分的技术。

被运用于众多使用大量小图标的网页应用之上。它可取图像的一部分来使用,使得使用一个图像文件替代多个小文件成为可能。相较于一个小图标一个图像文件,单独一张图片所需的 HTTP 请求更少,对内存和带宽更加友好。

Base64

不难看出,每次加载图片,都是须要单独向服务器请求这个图片对应的资源的——这也就意味着一次 HTTP 请求的开销。
Base64 是一种用于传输 8Bit 字节码的编码方式,经过对图片进行 Base64 编码,咱们能够直接将编码结果写入 HTML 或者写入 CSS,从而减小 HTTP 请求的次数

按照一向的思路,咱们加载图片须要把图片连接写入 img 标签:

<img src="https://user-gold-cdn.xitu.io/2018/9/15/165db7e94699824b?w=22&h=22&f=png&s=3680">

浏览器就会针对咱们的图片连接去发起一个资源请求.

可是若是咱们对这个图片进行 Base64 编码,咱们会获得一个这样的字符串:

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAMJGlDQ1BJQ0MgUHJvZmlsZQAASImVlwdUU8kagOeWJCQktEAEpITeBCnSpdfQpQo2QhJIKDEkBBU7uqjgWlARwYquitjWAshiw14Wwd4fiKgo62LBhsqbFNDV89477z9n7v3yzz9/mcydMwOAehxbJMpFNQDIExaI48MCmeNT05ikR4AECIAKRgEamyMRBcTFRQEoQ+9/yrubAJG9r9nLfP3c/19Fk8uTcABA4iBncCWcPMiHAMDdOCJxAQCEXqg3m1YggkyEWQJtMUwQsrmMsxTsIeMMBUfJbRLjgyCnA6BCZbPFWQCoyfJiFnKyoB+1pZAdhVyB

字符串比较长,咱们能够直接用这个字符串替换掉上文中的连接地址。你会发现浏览器原来是能够理解这个字符串的,它自动就将这个字符串解码为了一个图片,而不需再去发送 HTTP 请求

Base64 的应用场景

上面这个实例,其实源自咱们 掘金 网站 Header 部分的搜索栏 Logo:
Base64 编码后,图片大小会膨胀为原文件的 4/3(这是由 Base64 的编码原理决定的)。若是咱们把大图也编码到 HTML 或 CSS 文件中,后者的体积会明显增长,即使咱们减小了 HTTP 请求,也没法弥补这庞大的体积带来的性能开销,得不偿失。

在传输很是小的图片的时候,Base64 带来的文件体积膨胀、以及浏览器解析 Base64 的时间开销,与它节省掉的 HTTP 请求开销相比,能够忽略不计,这时候才能真正体现出它在性能方面的优点。

所以,Base64 并不是万全之策,咱们每每在一张图片知足如下条件时会对它应用 Base64 编码:

  • 图片的实际尺寸很小(你们能够观察一下掘金页面的 Base64 图,几乎没有超过 2kb 的)
  • 图片没法以雪碧图的形式与其它小图结合(合成雪碧图还是主要的减小 HTTP 请求的途径,Base64 是雪碧图的补充)
  • 图片的更新频率很是低(不需咱们重复编码和修改文件内容,维护成本较低)

    Base64 编码工具推荐

    这里最推荐的是利用 webpack 来进行 Base64 的编码——webpack 的 url-loader 很是聪明,它除了具有基本的 Base64 转码能力,还能够结合文件大小,帮咱们判断图片是否有必要进行 Base64 编码。

除此以外,市面上免费的 Base64 编解码工具种类是很是多样化的,有不少网站都提供在线编解码的服务,你们选取本身认为顺手的工具就好。

WebP

WebP 像 JPEG 同样对细节丰富的图片信手拈来,像 PNG 同样支持透明,像 GIF 同样能够显示动态图片——它集多种图片文件格式的优势于一身。

与 PNG 相比,WebP 无损图像的尺寸缩小了 26%。在等效的 SSIM 质量指数下,WebP 有损图像比同类 JPEG 图像小 25-34%。

无损 WebP 支持透明度(也称为 alpha 通道),仅需 22% 的额外字节。对于有损 RGB 压缩可接受的状况,有损 WebP 也支持透明度,与 PNG 相比,一般提供 3 倍的文件大小。
WebP 纵有千般好 都逃不开兼容性的大坑

此外,WebP 还会增长服务器的负担——和编码 JPG 文件相比,编码一样质量的 WebP 文件会占用更多的计算资源。

WebP 的应用场景

如今限制咱们使用 WebP 的最大问题不是“这个图片是否适合用 WebP 呈现”的问题,而是“浏览器是否容许 WebP”的问题,即咱们上文谈到的兼容性问题。具体来讲,一旦咱们选择了 WebP,就要考虑在 Safari 等浏览器下它没法显示的问题,也就是说咱们须要准备 PlanB,准备降级方案。

目前真正把 WebP 格式落地到网页中的网站并非不少,这其中淘宝首页对 WebP 兼容性问题的处理方式就很是有趣。咱们能够打开 Chrome 的开发者工具搜索其源码里的 WebP 关键字

<img src="//img.alicdn.com/tps/i4/TB1CKSgIpXXXXccXXXX07tlTXXX-200-200.png_60x60.jpg_.webp" alt="手机app - 聚划算" class="app-icon">

.webp 前面,还跟了一个 .jpg 后缀!
这个图片应该至少存在 jpg 和 webp 两种格式,程序会根据浏览器的型号、以及该型号是否支持 WebP 这些信息来决定当前浏览器显示的是 .webp 后缀仍是 .jpg 后缀。带着这个预判,咱们打开并不支持 WebP 格式的 Safari 来进入一样的页面,再次搜索 WebP 关键字:
Safari 提示咱们找不到,这也是情理之中。咱们定位到刚刚示例的 WebP 图片所在的元素,查看一下它在 Safari 里的图片连接

<img src="//img.alicdn.com/tps/i4/TB1CKSgIpXXXXccXXXX07tlTXXX-200-200.png_60x60.jpg" alt="手机app - 聚划算" class="app-icon">

在 Safari 中的后缀从 .webp 变成了 .jpg!
站点确实是先进行了兼容性的预判,在浏览器环境支持 WebP 的状况下,优先使用 WebP 格式,不然就把图片降级为 JPG 格式(本质是对图片的连接地址做简单的字符串切割)。
此外,还有另外一个维护性更强、更加灵活的方案——把判断工做交给后端,由服务器根据 HTTP 请求头部的 Accept 字段来决定返回什么格式的图片。当 Accept 字段包含 image/webp 时,就返回 WebP 格式的图片,不然返回原图。这种作法的好处是,当浏览器对 WebP 格式图片的兼容支持发生改变时,咱们也不用再去更新本身的兼容断定代码,只须要服务端像往常同样对 Accept 字段进行检查便可。

由此也能够看出,咱们 WebP 格式的局限性确实比较明显,若是决定使用 WebP,兼容性处理是必不可少的

浏览器缓存机制与缓存策略

缓存能够提升网络IO消耗 提升访问速度
经过网络获取内容既速度缓慢又开销巨大。较大的响应须要在客户端与服务器之间进行屡次往返通讯,这会延迟浏览器得到和处理内容的时间,还会增长访问者的流量费用。所以,缓存并重复利用以前获取的资源的能力成为性能优化的一个关键方面。
浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列以下

  1. Memory Cache
  2. Service Worker Cache
  3. HTTP Cache
  4. Push Cache

HTTP 缓存机制

分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的状况下,才会走协商缓存。

强缓存是利用 http 头中的 Expires 和 Cache-Control 两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据其中的 expires 和 cache-control 判断目标资源是否“命中”强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通讯。

命中强缓存的状况下,返回的 HTTP 状态码为 200

强缓存的实现:从 expires 到 cache-control

实现强缓存,过去咱们一直用 expires。
当服务器返回响应时,在 Response Headers 中将过时时间写入 expires 字段。像这样:

expires: Wed, 11 Sep 2019 16:12:18 GMT

expires 是一个时间戳,接下来若是咱们试图再次向服务器请求资源,浏览器就会先对比本地时间和 expires 的时间戳,若是本地时间小于 expires 设定的过时时间,那么就直接去缓存中取这个资源。

expires 是有问题的,它最大的问题在于对“本地时间”的依赖。若是服务端和客户端的时间设置可能不一样,或者我直接手动去把客户端的时间改掉,那么 expires 将没法达到咱们的预期。
考虑到 expires 的局限性,HTTP1.1 新增了 Cache-Control 字段来完成 expires 的任务。

expires 能作的事情,Cache-Control 都能作;expires 完成不了的事情,Cache-Control 也能作。所以,Cache-Control 能够视做是 expires 的彻底替代方案。在当下的前端实践里,咱们继续使用 expires 的惟一目的就是向下兼容。

如今咱们给 Cache-Control 字段一个特写:

cache-control: max-age=31536000

经过 max-age 来控制资源的有效期。max-age 不是一个时间戳,而是一个时间长度。在本例中,max-age 是 31536000 秒,它意味着该资源在 31536000 秒之内都是有效的,完美地规避了时间戳带来的潜在问题。
Cache-Control 相对于 expires 更加准确,它的优先级也更高。当 Cache-Control 与 expires 同时出现时,咱们以 Cache-Control 为准。
Cache-Control 的神通,可不止于这一个小小的 max-age。以下的用法也很是常见

cache-control: max-age=3600, s-maxage=31536000

s-maxage 优先级高于 max-age,二者同时出现时,优先考虑 s-maxage。若是 s-maxage 未过时,则向代理服务器请求其缓存内容。
在项目不是特别大的场景下,max-age 足够用了。但在依赖各类代理的大型架构中,咱们不得不考虑代理服务器的缓存问题。s-maxage 就是用于表示 cache 服务器上(好比 cache CDN)的缓存的有效时间的,并只对 public 缓存有效。
那么什么是 public 缓存呢

public 与 private

public 与 private 是针对资源是否可以被代理服务缓存而存在的一组对立概念。
若是咱们为资源设置了 public,那么它既能够被浏览器缓存,也能够被代理服务器缓存;若是咱们设置了 private,则该资源只能被浏览器缓存。private 为默认值。但多数状况下,public 并不须要咱们手动设置,好比有不少线上网站的 cache-control 是这样的:
image.png
设置了 s-maxage,没设置 public,那么 CDN 还能够缓存这个资源吗?答案是确定的。由于明确的缓存信息(例如“max-age”)已表示响应是能够缓存的。

no-store与no-cache

no-cache 绕开了浏览器:咱们为资源设置了 no-cache 后,每一次发起请求都不会再去询问浏览器的缓存状况,而是直接向服务端去确认该资源是否过时

no-store 比较绝情,顾名思义就是不使用任何缓存策略。在 no-cache 的基础上,它连服务端的缓存确认也绕开了,只容许你直接向服务端发送请求、并下载完整的响应。

协商缓存:浏览器与服务器合做之下的缓存策略

协商缓存依赖于服务端与浏览器之间的通讯。
协商缓存机制下,浏览器须要向服务器去询问缓存的相关信息,进而判断是从新发起请求、下载完整的响应,仍是从本地获取缓存的资源。
若是服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种状况下网络请求对应的状态码是 304(以下图)
image.png

协商缓存的实现:从 Last-Modified 到 Etag

Last-Modified 是一个时间戳,若是咱们启用了协商缓存,它会在首次请求时随着 Response Headers 返回:

Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT

随后咱们每次请求时,会带上一个叫 If-Modified-Since 的时间戳字段,它的值正是上一次 response 返回给它的 last-modified 值:

If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT

服务器接收到这个时间戳后,会比对该时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化。若是发生了变化,就会返回一个完整的响应内容,并在 Response Headers 中添加新的 Last-Modified 值;不然,返回如上图的 304 响应,Response Headers 不会再添加 Last-Modified 字段。
使用 Last-Modified 存在一些弊端,这其中最多见的就是这样两个场景:
1 咱们编辑了文件,但文件的内容没有改变。服务端并不清楚咱们是否真正改变了文件,它仍然经过最后编辑时间进行判断。所以这个资源在再次被请求时,会被当作新资源,进而引起一次完整的响应——不应从新请求的时候,也会从新请求。
2 当咱们修改文件的速度过快时(好比花了 100ms 完成了改动),因为 If-Modified-Since 只能检查到以秒为最小计量单位的时间差,因此它是感知不到这个改动的——该从新请求的时候,反而没有从新请求了。

这两个场景其实指向了同一个 bug——服务器并无正确感知文件的变化。为了解决这样的问题,Etag 做为 Last-Modified 的补充出现了。

Etag 是由服务器为每一个资源生成的惟一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不一样,它们对应的 Etag 就是不一样的,反之亦然。所以 Etag 可以精准地感知文件的变化
Etag 和 Last-Modified 相似,当首次请求时,咱们会在响应头里获取到一个最初的标识符字符串,举个🌰,它能够是这样的:

ETag: W/"2a3b-1602480f459"

那么下一次请求时,请求头里就会带上一个值相同的、名为 if-None-Match 的字符串供服务端比对了:

If-None-Match: W/"2a3b-1602480f459"

Etag 的生成过程须要服务器额外付出开销,会影响服务端的性能,这是它的弊端。所以启用 Etag 须要咱们审时度势。正如咱们刚刚所提到的——Etag 并不能替代 Last-Modified,它只能做为 Last-Modified 的补充和强化存在。
Etag 在感知文件变化上比 Last-Modified 更加准确,优先级也更高。当 Etag 和 Last-Modified 同时存在时,以 Etag 为准。
image.png
解读一下这张流程图
当咱们的资源内容不可复用时,直接为 Cache-Control 设置 no-store,拒绝一切形式的缓存;不然考虑是否每次都须要向服务器进行缓存有效确认,若是须要,那么设 Cache-Control 的值为 no-cache;不然考虑该资源是否能够被代理服务器缓存,根据其结果决定是设置为 private 仍是 public;而后考虑该资源的过时时间,设置对应的 max-age 和 s-maxage 值;最后,配置协商缓存须要用到的 Etag、Last-Modified 等参数。

MemoryCache

MemoryCache,是指存在内存中的缓存。从优先级上来讲,它是浏览器最早尝试去命中的一种缓存。从效率上来讲,它是响应速度最快的一种缓存。
内存缓存是快的,也是“短命”的。它和渲染进程“生死相依”,当进程结束后,也就是 tab 关闭之后,内存里的数据也将不复存在。

资源存不存内存,浏览器秉承的是“节约原则”。咱们发现,Base64 格式的图片,几乎永远能够被塞进 memory cache,这能够视做浏览器为节省渲染开销的“自保行为”;此外,体积不大的 JS、CSS 文件,也有较大地被写入内存的概率——相比之下,较大的 JS、CSS 文件就没有这个待遇了,内存资源是有限的,它们每每被直接甩进磁盘

Service Worker Cache

Service Worker 是一种独立于主线程以外的 Javascript 线程。它脱离于浏览器窗体,所以没法直接访问 DOM。
这样独立的个性使得 Service Worker 的“我的行为”没法干扰页面的性能,这个“幕后工做者”能够帮咱们实现离线缓存、消息推送和网络代理等功能。
咱们借助 Service worker 实现的离线缓存就称为 Service Worker Cache
Service Worker 的生命周期包括 install、active、working 三个阶段。一旦 Service Worker 被 install,它将始终存在,只会在 active 与 working 之间切换,除非咱们主动终止它。这是它能够用来实现离线存储的重要先决条件。
Service Worker 如何为咱们实现离线缓存(注意看注释):入口文件中插入这样一段 JS 代码,用以判断和引入 Service Worker:

window.navigator.serviceWorker.register('/test.js').then(()=>{
console.log('注册成功')
}).catch((error)=>{
    console.log('注册失败')
})

在 test.js 中,咱们进行缓存的处理。假设咱们须要缓存的文件分别是 test.html,test.css 和 test.js:

self.addEventListener('install',event=>{
          event.waitUntill(
            // 考虑到缓存也须要更新, open内传入的参数为缓存的版本号
            caches.open('test-v1').then(cache=>{
              return cache.addAll([
                //此处传入指定的需缓存的文件名
                '/test.html',
                '/test.css',
                'test.js'
              ])
            })
          )
        })
        //Service Worker会监听全部的网络请求,网络请求的产生触发的是fetch事件,咱们能够在其对应的监听函数中实现对请求的拦截
        //进而判断是否对应到该请求的缓存 实现从Service Worker中取缓存的目的
        self.addEventListener('fetch',event=>{
          event.respondWith(
            //尝试匹配该请求对应的缓存值
            caches.match(event.request).then(res=>{
              //若是匹配到了,调用Server Worker缓存
              if(res){
                return res
              }
              //若是没有匹配到  向服务器发起这个资源请求
              return fetch(event.request).then(response=>{
                if(!response||response.status!==200){
                  return response
                }
                //请求成功的话,将请求缓存起来
                caches.open('test-v1').then((cache)=>{
                  cache.put(event.request,response)
                })
                return response.clone()
              })
            })
          )
        })

Server Worker 对协议是有要求的,必须以 https 协议为前提。

Push Cache

https://jakearchibald.com/201...

Push Cache 是指 HTTP2 在 server push 阶段存在的缓存。这块的知识比较新,应用也还处于萌芽阶段,
但应用范围有限不表明不重要——HTTP2 是趋势、是将来。

*Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均未命中的状况下才会去询问 Push Cache。

*Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。
*不一样的页面只要共享了同一个 HTTP2 链接,那么它们就能够共享同一个 Push Cache。

本地存储——从 Cookie 到 Web Storage、IndexDB

从 Cookie 提及

HTTP 协议是一个无状态协议,服务器接收客户端的请求,返回一个响应 服务器并无记录下关于客户端的任何信息。
Cookie 说白了就是一个存储在浏览器里的一个小小的文本文件,它附着在 HTTP 请求上,在浏览器和服务器之间“飞来飞去”。它能够携带用户信息,当服务器检查 Cookie 的时候,即可以获取到客户端的状态。

Cookie的性能劣势

Cookie 不够大

Cookie 是有体积上限的,它最大只能有 4KB。当 Cookie 超过 4KB 时,它将面临被裁切的命运。这样看来,Cookie 只能用来存取少许的信息。

过量的 Cookie 会带来巨大的性能浪费

Cookie 是紧跟域名的。咱们经过响应头里的 Set-Cookie 指定要存储的 Cookie 值。默认状况下,domain 被设置为设置 Cookie 页面的主机名,咱们也能够手动设置 domain 的值:

Set-Cookie: name=xiuyan; domain=xiuyan.me

同一个域名下的全部请求,都会携带 Cookie

请求一张图片或者一个 CSS 文件,咱们也要携带一个 Cookie 跑来跑去(关键是 Cookie 里存储的信息我如今并不须要),这是一件多么劳民伤财的事情。Cookie 虽然小,请求却能够有不少,随着请求的叠加,这样的没必要要的 Cookie 带来的开销将是没法想象的。

Web Storage

Web Storage 是 HTML5 专门为浏览器存储而提供的数据存储机制。它又分为 Local Storage 与 Session Storage。
二者的区别在于生命周期做用域的不一样。

  • 生命周期:Local Storage 是持久化的本地存储,存储在其中的数据是永远不会过时的,使其消失的惟一办法是手动删除;而 Session Storage 是临时性的本地存储,它是会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放。
  • 做用域:Local Storage、Session Storage 和 Cookie 都遵循同源策略。但 Session Storage 特别的一点在于,即使是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 Session Storage 内容便没法共享。

Web Storage 的特性

  • 存储容量大: Web Storage 根据浏览器的不一样,存储容量能够达到 5-10M 之间。
  • 仅位于浏览器端,不与服务端发生通讯。
  • Web Storage 核心 API 使用示例

Web Storage 保存的数据内容和 Cookie 同样,是文本内容,以键值对的形式存在。Local Storage 与 Session Storage 在 API 方面无异,这里咱们以 localStorage 为例:

  • 存储数据:setItem()
localStorage.setItem('user_name', 'xiuyan')
  • 读取数据: getItem()
localStorage.getItem('user_name')
  • 删除某一键名对应的数据: removeItem()
localStorage.removeItem('user_name')
  • 清空数据记录:clear()
localStorage.clear()

应用场景

倾向于用它来存储一些内容稳定的资源。好比图片内容丰富的电商网站会用它来存储 Base64 格式的图片字符串:
有的网站还会用它存储一些不常常更新的 CSS、JS 等静态资源。

Session Storage

Session Storage 更适合用来存储生命周期和它同步的会话级别的信息。这些信息只适用于当前会话,当你开启新的会话时,它也须要相应的更新或释放。好比微博的 Session Storage 就主要是存储你本次会话的浏览足迹:
image.png
lasturl 对应的就是你上一次访问的 URL 地址,这个地址是即时的。当你切换 URL 时,它随之更新,当你关闭页面时,留着它也确实没有什么意义了,干脆释放吧。这样的数据用 Session Storage 来处理再合适不过
Web Storage 是一个从定义到使用都很是简单的东西。它使用键值对的形式进行存储,这种模式有点相似于对象,却甚至连对象都不是——它只能存储字符串,要想获得对象,咱们还须要先对字符串进行一轮解析。

说到底,Web Storage 是对 Cookie 的拓展,它只能用于存储少许的简单数据。当遇到大规模的、结构复杂的数据时,Web Storage 也心有余而力不足了。这时候咱们就要清楚咱们的终极大 boss——IndexDB!

终极形态:IndexDB

IndexDB 是一个运行在浏览器上的非关系型数据库。既然是数据库了,那就不是 5M、10M 这样小打小闹级别了。理论上来讲,IndexDB 是没有存储上限的(通常来讲不会小于 250M)。它不只能够存储字符串,还能够存储二进制数据。

遵循 MDN 推荐的操做模式 操做一个基本的 IndexDB 使用流程
1 打开/建立一个 IndexDB 数据库(当该数据库不存在时,open 方法会直接建立一个名为 xiaoceDB 新数据库)。

//后面的回调中  咱们能够经过event.target.result拿到数据库实例
       let db
       //参数1位数据库名  参数2为版本号
       const request = window.indexedDB.open('xiaoceDB',1)
       //使用IndexDB失败时的监听函数
       request.onerror = function(event){
         console.log('没法使用IndexDB');
       }
       //成功
       request.onsuccess = function(event){
         //此处就能够获取到db实例
         db = event.target.result
         console.log('您打开了IndexDB');
       }

2 建立一个object store(object store对标到数据库中的表单位)

//onupgradeneeded事件会在初始化数据库/版本发生更新时调用,咱们在它的监听函数中建立object store
              request.onupgradeneeded = function(event){
                let objectStore
                //若是同名表未被建立过 则新建test表
                if(!db.objectStoreNames.contains('test')){
                  objectStore = db.createObjectStore('test',{keyPath:'id'})
                }
              }

3 构建一个事务来执行一些数据库操做,像增长或提取数据等。

//建立事务  指定表格名称和读写功能
             const transaction = db.transaction(["test"],"readwrite")
            //  拿到Object Store对象
            const objectStore = transaction.objectStore("test")
            //向表格写入数据
            objectStore.add({id:1,name:'xiuyan'})

4 经过监听正确类型的事件以等待操做完成。

// 操做完成时的监听函数
            transaction.oncomplete = function(event){
              console.log('操做完成')
            }
            // 操做失败时的监听函数
            transaction.onerror = function(event){
              console.log('这里有一个error')
            }

IndexDB 的应用场景

在 IndexDB 中,咱们能够建立多个数据库,一个数据库中建立多张表,一张表中存储多条数据——这足以 hold 住复杂的结构性数据。IndexDB 能够看作是 LocalStorage 的一个升级,当数据的复杂度和规模上升到了 LocalStorage 没法解决的程度,咱们毫无疑问能够请出 IndexDB 来帮忙。

浏览器缓存/存储技术的出现和发展,为咱们的前端应用带来了无限的起色。近年来基于缓存/存储技术的第三方库层出不绝,此外还衍生出了 PWA 这样优秀的 Web 应用模型。能够说,现代前端应用,尤为是移动端应用,之因此能够发展到在体验上叫板 Native 的地步,主要就是仰仗缓存/存储立下的汗马功劳

CDN 的缓存与回源机制解析

CDN (Content Delivery Network,即内容分发网络)指的是一组分布在各个地区的服务器。这些服务器存储着数据的副本,所以服务器能够根据哪些服务器与用户距离最近,来知足数据的请求。 CDN 提供快速服务,较少受高流量影响

缓存、本地存储带来的性能提高,是否是只能在“获取到资源并把它们存起来”这件事情发生以后?也就是说,首次请求资源的时候,这些招数都是救不了咱们的。要提高首次请求的响应能力,咱们还须要借助 CDN 的能力

CDN 如何工做

*假设个人根服务器在杭州
此时有一位北京的用户向我请求资源。在网络带宽小、用户访问量大的状况下,杭州的这一台服务器或许不那么给力,不能给用户很是快的响应速度。因而我灵机一动,把这批资源 copy 了一批放在北京的机房里。当用户请求资源时,就近请求北京的服务器,北京这台服务器低头一看,这个资源我存了,离得这么近,响应速度确定噌噌的!那若是北京这台服务器没有 copy 这批资源呢?它会再向杭州的根服务器去要这个资源。在这个过程当中,北京这台服务器就扮演着 CDN 的角色。*

CDN的核心功能特写

CDN 的核心点有两个,一个是缓存,一个是回源

“缓存”就是说咱们把资源 copy 一份到 CDN 服务器上这个过程,“回源”就是说 CDN 发现本身没有这个资源(通常是缓存的数据过时了),转头向根服务器(或者它的上层服务器)去要这个资源的过程。

CDN 与前端性能优化

CDN 每每被用来存放静态资源。上文中咱们举例所提到的“根服务器”本质上是业务服务器,它的核心任务在于生成动态页面或返回非纯静态页面,这两种过程都是须要计算的。业务服务器仿佛一个车间,车间里运转的机器轰鸣着为咱们产出所需的资源;相比之下,CDN 服务器则像一个仓库,它只充当资源的“栖息地”和“搬运工”。
所谓“静态资源”,就是像 JS、CSS、图片等不须要业务服务器进行计算即得的资源。而“动态资源”,顾名思义是须要后端实时动态生成的资源,较为常见的就是 JSP、ASP 或者依赖服务端渲染获得的 HTML 页面。
什么是“非纯静态资源”呢?它是指须要服务器在页面以外做额外计算的 HTML 页面。具体来讲,当我打开某一网站以前,该网站须要经过权限认证等一系列手段确认个人身份、进而决定是否要把 HTML 页面呈现给我。这种状况下 HTML 确实是静态的,但它和业务服务器的操做耦合,咱们把它丢到CDN 上显然是不合适的。

CDN 的实际应用

静态资源自己具备访问频率高、承接流量大的特色,所以静态资源加载速度始终是前端性能的一个很是关键的指标。CDN 是静态资源提速的重要手段,在许多一线的互联网公司,“静态资源走 CDN”并非一个建议,而是一个规定。
好比以淘宝为表明的阿里系产品,就遵循着这个“规定”。

打开淘宝首页,咱们能够在 Network 面板中看到,“非纯静态”的 HTML 页面,是向业务服务器请求来的:
image.png
咱们点击 preview,能够看到业务服务器确实是返回给了咱们一个还没有被静态资源加持过的简单 HTML 页面,全部的图片内容都是先以一个 div 占位:
image.png

相应地,咱们随便点开一个静态资源,能够看到它都是从 CDN 服务器上请求来的。

好比说图片:
image.png

再好比 JS、CSS 文件:
image.png

CDN 优化细节

如何让 CDN 的效用最大化?这又是须要先后端程序员一块儿思考的庞大命题。它涉及到 CDN 服务器自己的性能优化、CDN 节点的地址选取等。谈离前端最近的这部分细节:CDN 的域名选取。
淘宝首页的例子,咱们注意到业务服务器的域名是这个:

www.taobao.com

而 CDN 服务器的域名是这个

g.alicdn.com

咱们讲到 Cookie 的时候,为了凸显 Local Storage 的优越性,曾经提到过
同一个域名下的请求会不分青红皂白地携带 Cookie,而静态资源每每并不须要 Cookie 携带什么认证信息。把静态资源和主页面置于不一样的域名下,完美地避免了没必要要的 Cookie 的出现!
看起来是一个不起眼的小细节,但带来的效用倒是惊人的。以电商网站静态资源的流量之庞大,若是没把这个多余的 Cookie 拿下来,不只用户体验会大打折扣,每一年因性能浪费带来的经济开销也将是一个很是恐怖的数字。

如此看来,性能优化还真是要步步为营!

服务端渲染的探索与实践、

服务端渲染的运行机制

客户端渲染

客户端渲染模式下,服务端会把渲染须要的静态文件发送给客户端,客户端加载过来以后,本身在浏览器里跑一遍 JS,根据 JS 的运行结果,生成相应的 DOM。这种特性使得客户端渲染的源代码老是特别简洁,

<!doctype html>
<html>
  <head>
    <title>我是客户端渲染的页面</title>
  </head>
  <body>
    <div id='root'></div>
    <script src='index.js'></script>
  </body>
</html>

根节点下究竟是什么内容呢?你不知道,我不知道,只有浏览器把 index.js 跑过一遍后才知道,这就是典型的客户端渲染。

页面上呈现的内容,你在 html 源文件里里找不到——这正是它的特色。

服务端渲染

服务端渲染的模式下,当用户第一次请求页面时,由服务器把须要的组件或页面渲染成 HTML 字符串,而后把它返回给客户端。客户端拿到手的,是能够直接渲染而后呈现给用户的 HTML 内容,不须要为了生成 DOM 内容本身再去跑一遍 JS 代码。
使用服务端渲染的网站,能够说是“所见即所得”,页面上呈现的内容,咱们在 html 源文件里也能找到

好比知乎就是典型的服务端渲染案例:

服务端渲染解决了什么性能问题

事实上,不少网站是出于效益的考虑才启用服务端渲染,性能却是在其次。
假设 A 网站页面中有一个关键字叫“前端性能优化”,这个关键字是 JS 代码跑过一遍后添加到 HTML 页面中的。那么客户端渲染模式下,咱们在搜索引擎搜索这个关键字,是找不到 A 网站的——搜索引擎只会查找现成的内容,不会帮你跑 JS 代码。A 网站的运营方见此情形,感到很头大:搜索引擎搜不出来,用户找不到咱们,谁还会用个人网站呢?为了把“现成的内容”拿给搜索引擎看,A 网站不得不启用服务端渲染。

但性能在其次,不表明性能不重要。服务端渲染解决了一个很是关键的性能问题——首屏加载速度过慢。在客户端渲染模式下,咱们除了加载 HTML,还要等渲染所需的这部分 JS 加载完,以后还得把这部分 JS 在浏览器上再跑一遍。这一切都是发生在用户点击了咱们的连接以后的事情,在这个过程结束以前,用户始终见不到咱们网页的庐山真面目,也就是说用户一直在等!相比之下,服务端渲染模式下,服务器给到客户端的已是一个直接能够拿来呈现给用户的网页,中间环节早在服务端就帮咱们作掉了,用户岂不“美滋滋”?

服务端渲染的应用实例

先来看一下在一个 React 项目里,服务端渲染是怎么实现的。本例中,咱们使用 Express 搭建后端服务。

项目中有一个叫作 VDom 的 React 组件,它的内容以下。

VDom.js:

import React from 'react'
           const VDom = ()=>{
             return <div>我是一个被渲染为真是DOM的虚拟DOM</div>
           }
           export default VDom

在服务端的入口文件中,我引入这个组件,对它进行渲染:

import express from 'express'
        import React from 'react'
        import {renderToString} from 'react-dom/server'
        import VDom from './VDom'
        // 建立一个express应用
        const app = express()
        //renderToString 是把虚拟DOM转化为真实DOM内容
        const Page = `
         <html>
            <head>
              <title>test</title>
            </head>
            <body>
              <span>服务端渲染出了真实DOM:</span>
              ${RDom}
            </body>
        </html>
        `
        //配置HTML内容对应的路由
        app.get('/index',function(req,res){
          res.send(Page)
        })
        // 配置端口号
        const server = app.listen(8000)

根据咱们的路由配置,当我访问 http://localhost:8000/index 时,就能够呈现出服务端渲染的结果了:

咱们能够看到,VDom 组件已经被 renderToString 转化为了一个内容为<div data-reactroot="">我是一个被渲染为真实DOM的虚拟DOM</div>的字符串,这个字符串被插入 HTML 代码,成为了真实 DOM 树的一部分。
那么 Vue 是如何实现服务端渲染的呢?
该示例直接将 Vue 实例整合进了服务端的入口文件中:

const Vue  = require('vue')
        // 建立一个express应用
        const server = require('express')()
          //提取出renderer实例
        const renderer = require('vue-server-renderer').createRenderer()
        server.get('*',(req,res)=>{
          // 编写Vue实例(虚拟DOM节点)
          const app = new Vue({
            data:{
              url:req.url
            },
            // 编写模板HTML的内容
            template:`<div>访问的URL是:{{url}}</div>`
          })
          // renderToString是把Vue实例转换为真实DOM的关键方法
          renderer.renderToString(app,(err,html)=>{
            if(err){
              res.status(500).end("Internal Server Error")
              return
            }
            // 把渲染出来的真实DOM字符串插入HTML模板中
            res.end(`
               <!DOCTYPE HTML>
                <html>
                  <head>
                    <title>hello</title>
                  </head>
                  <body>
                    ${html}
                  </body>
                </html>
            `)
          })
        })
        server.listen(8080)

实际项目比这些复杂不少,但万变不离其宗。强调的只有两点:一是这个 renderToString() 方法;二是把转化结果“塞”进模板里的这一步。这两个操做是服务端渲染的灵魂操做。在虚拟 DOM“横行”的当下,服务端渲染再也不是早年 JSP 里简单粗暴的字符串拼接过程,它还要求这一端要具有将虚拟 DOM 转化为真实 DOM 的能力。与其说是“把 JS 在服务器上先跑一遍”,不如说是“把 Vue、React 等框架代码先在 Node 上跑一遍”。

服务端渲染的应用场景

服务端渲染本质上是本该浏览器作的事情,分担给服务器去作。这样当资源抵达浏览器时,它呈现的速度就快了。乍一看好像很合理:浏览器性能毕竟有限,服务器多牛逼!能者多劳,就该让服务器多干点活!

但仔细想一想,在这个网民遍地的时代,几乎有多少个用户就有多少台浏览器。用户拥有的浏览器总量多到数不清,那么一个公司的服务器又有多少台呢?咱们把这么多台浏览器的渲染压力集中起来,分散给相比之下数量并很少的服务器,服务器确定是承受不住的。服务端渲染也并不是万全之策。
在实践中,建议你们先忘记服务端渲染这个事情——服务器稀少而宝贵,但首屏渲染体验和 SEO 的优化方案却不少——咱们最好先把能用的低成本“大招”都用完。除非网页对性能要求过高了,以致于全部的招式都用完了,性能表现仍是不尽人意,这时候咱们就能够考虑向老板多申请几台服务器,把服务端渲染搞起来了~

浏览器背后的运行机制

目前市面上常见的浏览器内核能够分为这四种:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。
可能会据说过 Chrome 的内核就是 Webkit,却不知 Chrome 内核早已迭代为了 Blink。可是换汤不换药,Blink 其实也是基于 Webkit 衍生而来的一个分支,所以,Webkit 内核仍然是当下浏览器世界真正的霸主。

什么是渲染过程?简单来讲,渲染引擎根据 HTML 文件描述构建相应的数学模型,调用浏览器各个零部件,从而将网页资源代码转换为图像结果,这个过程就是渲染过程

咱们最须要关注的,就是HTML 解释器、CSS 解释器、图层布局计算模块、视图绘制模块与JavaScript 引擎这几大模块:

  • HTML 解释器:将 HTML 文档通过词法分析输出 DOM 树。
  • CSS 解释器:解析 CSS 文档, 生成样式规则。
  • 图层布局计算模块:布局计算每一个对象的精确位置和大小。
  • 视图绘制模块:进行具体节点的图像绘制,将像素渲染到屏幕上。
  • JavaScript 引擎:编译执行 Javascript 代码。
浏览器渲染过程解析

在这里插入图片描述

  • 解析 HTML

在这一步浏览器执行了全部的加载解析逻辑,在解析 HTML 的过程当中发出了页面渲染所需的各类外部资源请求。

  • 计算样式

浏览器将识别并加载全部的 CSS 样式信息与 DOM 树合并,最终生成页面 render 树(:after :before 这样的伪元素会在这个环节被构建到 DOM 树中)。

  • 计算图层布局
    页面中全部元素的相对位置信息,大小等信息均在这一步获得计算。

    • 绘制图层

    在这一步中浏览器会根据咱们的 DOM 代码结果,把每个页面图层转换为像素,并对全部的媒体文件进行解码。

    • 整合图层,获得页面

    最后一步浏览器会合并合各个图层,将数据由 CPU 输出给 GPU 最终绘制在屏幕上。(复杂的视图层会给这个阶段的 GPU 计算带来一些压力,在实际应用中为了优化动画性能,咱们有时会手动区分不一样的图层)。
    段的 GPU 计算带来一些压力,在实际应用中为了优化动画性能,咱们有时会手动区分不一样的图层)。

几棵重要的“树”
在这里插入图片描述

  • DOM 树:解析 HTML 以建立的是 DOM 树(DOM tree ):渲染引擎开始解析 HTML 文档,转换树中的标签到 DOM 节点,它被称为“内容树”。
  • CSSOM 树:解析 CSS(包括外部 CSS 文件和样式元素)建立的是 CSSOM 树。CSSOM 的解析过程与 DOM 的解析过程是并行的。
  • 渲染树:CSSOM 与 DOM 结合,以后咱们获得的就是渲染树(Render tree )。
  • 布局渲染树:从根节点递归调用,计算每个元素的大小、位置等,给每一个节点所应该出如今屏幕上的精确坐标,咱们便获得了基于渲染树的布局渲染树(Layout of the render tree)。
  • 绘制渲染树: 遍历渲染树,每一个节点将使用 UI 后端层来绘制。整个过程叫作绘制渲染树(Painting the render tree)。

渲染过程说白了,首先是基于 HTML 构建一个 DOM 树,这棵 DOM 树与 CSS 解释器解析出的 CSSOM 相结合,就有了布局渲染树。最后浏览器以布局渲染树为蓝本,去计算布局并绘制图像,咱们页面的初次渲染就大功告成了。
基于渲染流程的 CSS 优化建议
CSS 引擎查找样式表,对每条规则都按从右到左的顺序去匹配。 看以下规则:

#myList  li {}

习惯了从左到右阅读的文字阅读方式,会本能地觉得浏览器也是从左到右匹配 CSS 选择器的,所以会推测这个选择器并不会费多少力气:#myList 是一个 id 选择器,它对应的元素只有一个,查找起来应该很快。定位到了 myList 元素,等因而缩小了范围后再去查找它后代中的 li 元素,没毛病。

事实上,CSS 选择符是从右到左进行匹配的。咱们这个看似“没毛病”的选择器,实际开销至关高:浏览器必须遍历页面上每一个 li 元素,而且每次都要去确认这个 li 元素的父元素 id 是否是 myList

总结出以下性能提高的方案:

  • 避免使用通配符,只对须要用到的元素进行选择。
  • 关注能够经过继承实现的属性,避免重复匹配重复定义。
  • 少用标签选择器。若是能够,用类选择器替代,举个🌰:
  • 错误示范:
- #myList li{}

正确:

.myList_li {}
  • 减小嵌套。后代选择器的开销是最高的,所以咱们应该尽可能将选择器的深度降到最低(最高不要超过三层),尽量使用类来关联每个标签元素。

CSS 与 JS 的加载顺序优化

CSS 的阻塞

DOM 和 CSSOM 协力才能构建渲染树。这一点会给性能形成严重影响:默认状况下,CSS 是阻塞的资源。浏览器在构建 CSSOM 的过程当中,不会渲染任何已处理的内容。即使 DOM 已经解析完毕了,只要 CSSOM 不 OK,那么渲染这个事情就不 OK(这主要是为了不没有 CSS 的 HTML 页面丑陋地“裸奔”在用户眼前)。
只有当咱们开始解析 HTML 后、解析到 link 标签或者 style 标签时,CSS 才登场,CSSOM 的构建才开始。不少时候,DOM 不得不等待 CSSOM。

CSS 是阻塞渲染的资源。须要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。
  • 将 CSS 放在 head 标签里 和尽快(启用 CDN 实现静态资源加载速度的优化)
JS 的阻塞

JS 的做用在于修改,它帮助咱们修改网页的方方面面:内容、样式以及它如何响应用户交互。这“方方面面”的修改,本质上都是对 DOM 和 CSSDOM 进行修改。所以 JS 的执行会阻止 CSSOM,在咱们不做显式声明的状况下,它也会阻塞 DOM。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>JS阻塞测试</title>
  <style>
    #container {
      background-color: yellow;
      width: 100px;
      height: 100px;
    }
  </style>
  <script>
    // 尝试获取container元素
    var container = document.getElementById("container")
    console.log('container', container)
  </script>
</head>
<body>
  <div id="container"></div>
  <script>
    // 尝试获取container元素
    var container = document.getElementById("container")
    console.log('container', container)
    // 输出container元素此刻的背景色
    console.log('container bgColor', getComputedStyle(container).backgroundColor)
  </script>
  <style>
    #container {
      background-color: blue;
    }
  </style>
</body>
</html>

三个 console 的结果分别为:
在这里插入图片描述
第一次尝试获取 id 为 container 的 DOM 失败,这说明 JS 执行时阻塞了 DOM,后续的 DOM 没法构建;第二次才成功,这说明脚本块只能找到在它前面构建好的元素。这二者结合起来,“阻塞 DOM”获得了验证。再看第三个 console,尝试获取 CSS 样式,获取到的是在 JS 代码执行前的背景色(yellow),而非后续设定的新样式(blue),说明 CSSOM 也被阻塞了。
JS 引擎是独立于渲染引擎存在的。咱们的 JS 代码在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标签时,它会暂停渲染过程,将控制权交给 JS 引擎。JS 引擎对内联的 JS 代码会直接执行,对外部 JS 文件还要先获取到脚本、再进行执行。等 JS 引擎运行完毕,浏览器又会把控制权还给渲染引擎,继续 CSSOM 和 DOM 的构建。 所以与其说是 JS 把 CSS 和 HTML 阻塞了,不如说是 JS 引擎抢走了渲染引擎的控制权。
能够经过对它使用 defer 和 async 来避免没必要要的阻塞,这里咱们就引出了外部 JS 的三种加载方式。

JS的三种加载方式

- 正常模式:

<script src="index.js"></script>

这种状况下 JS 会阻塞浏览器,浏览器必须等待 index.js 加载和执行完毕才能去作其它事情
async 模式:

<script async src="index.js"></script>

async 模式下,JS 不会阻塞浏览器作任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会当即执行。

defer 模式:

<script defer src="index.js"></script>

defer 模式下,JS 的加载是异步的,执行是被推迟的。等整个文档解析完成、DOMContentLoaded 事件即将被触发时,被标记了 defer 的 JS 文件才会开始依次执行。

脚本与 DOM 元素和其它脚本之间的依赖关系不强时,咱们会选用 async;当脚本依赖于 DOM 元素和其它脚本的执行结果时,咱们会选用 defer。
经过审时度势地向 script 标签添加 async/defer,咱们就能够告诉浏览器在等待脚本可用期间不阻止其它的工做,这样能够显著提高性能。

DOM 优化原理与基本实践

把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁链接

JS 是很快的,在 JS 中修改 DOM 对象也是很快的。在JS的世界里,一切是简单的、迅速的。但 DOM 操做并不是 JS 一我的的独舞,而是两个模块之间的协做。
JS 引擎和渲染引擎(浏览器内核)是独立实现的。当咱们用 JS 去操做 DOM 时,本质上是 JS 引擎和渲染引擎之间进行了“跨界交流”。这个“跨界交流”的实现并不简单,它依赖了桥接接口做为“桥梁”
在这里插入图片描述

过“桥”要收费——这个开销自己就是不可忽略的。咱们每操做一次 DOM(无论是为了修改仍是仅仅为了访问其值),都要过一次“桥”。过“桥”的次数一多,就会产生比较明显的性能问题

对 DOM 的修改引起样式的更迭

过桥很慢,到了桥对岸,咱们的更改操做带来的结果也很慢。

不少时候,咱们对 DOM 的操做都不会局限于访问,而是为了修改它。当咱们对 DOM 的修改会引起它外观(样式)上的改变时,就会触发回流或重绘。
个过程本质上仍是由于咱们对 DOM 的修改触发了渲染树(Render Tree)的变化所致使的:
在这里插入图片描述
回流:当咱们对 DOM 的修改引起了 DOM 几何尺寸的变化(好比修改元素的宽、高或隐藏元素等)时,浏览器须要从新计算元素的几何属性(其余元素的几何属性和位置也会所以受到影响),而后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。

重绘:当咱们对 DOM 的修改致使了样式的变化、却并未影响其几何属性(好比修改了颜色或背景色)时,浏览器不需从新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。这个过程叫作重绘。

由此咱们能够看出,重绘不必定致使回流,回流必定会致使重绘。硬要比较的话,回流比重绘作的事情更多,带来的开销也更大。

给你的 DOM “提提速”

减小 DOM 操做:少交“过路费”、避免过分渲染

<!DOCTYPE html>
  <html lang="en"><head>  
  <meta charset="UTF-8">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  
  <meta http-equiv="X-UA-Compatible" content="ie=edge">  
  <title>DOM操做测试</title>
  </head>
  <body>  
  <div id="container"></div>
  </body>
  </html>

此时我有一个假需求——我想往 container 元素里写 10000 句同样的话。若是我这么作:

for(var count=0;count<10000;count++){ 
  document.getElementById('container').innerHTML+='<span>我是一个小测试</span>'
}

这段代码有两个明显的可优化点。

第一点,过路费交太多了。咱们每一次循环都调用 DOM 接口从新获取了一次 container 元素,至关于每次循环都交了一次过路费。先后交了 10000 次过路费,但其中 9999 次过路费均可以用缓存变量的方式节省下来:

// 只获取一次container
let container = document.getElementById('container')
for(let count=0;count<10000;count++){ 
  container.innerHTML += '<span>我是一个小测试</span>'
}

第二点,没必要要的 DOM 更改太多了。咱们的 10000 次循环里,修改了 10000 次 DOM 树。咱们前面说过,对 DOM 的修改会引起渲染树的改变、进而去走一个(可能的)回流或重绘的过程,而这个过程的开销是很“贵”的。这么贵的操做,咱们居然重复执行了 N 屡次!其实咱们能够经过就事论事的方式节省下来没必要要的渲染:

let container = document.getElementById('container')
let content = ''
for(let count=0;count<10000;count++){ 
  // 先对内容进行操做
  content += '<span>我是一个小测试</span>'
} 
// 内容处理好了,最后再触发DOM的更改
container.innerHTML = content

JS 层面的事情,JS 本身去处理,处理好了,再来找 DOM 打报告
事实上,考虑JS 的运行速度,比 DOM 快得多这个特性。咱们减小 DOM 操做的核心思路,就是让 JS 去给 DOM 分压。
这个思路,在 DOM Fragment 中体现得淋漓尽致。

DocumentFragment

DocumentFragment 接口表示一个没有父级文件的最小文档对象。它被当作一个轻量版的 Document 使用,用于存储已排好版的或还没有打理好格式的XML片断。由于 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会引发 DOM 树的从新渲染的操做(reflow),且不会致使性能等问题
在咱们上面的例子里,字符串变量 content 就扮演着一个 DOM Fragment 的角色。其实不管字符串变量也好,DOM Fragment 也罢,它们本质上都做为脱离了真实 DOM 树的容器出现,用于缓存批量化的 DOM 操做。

前面咱们直接用 innerHTML 去拼接目标内容,这样作当然有用,但却不够优雅。相比之下,DOM Fragment 能够帮助咱们用更加结构化的方式去达成一样的目的,从而在维持性能的同时,保住咱们代码的可拓展和可维护性。咱们如今用 DOM Fragment 来改写上面的例子:

let container = document.getElementById('container')
        // 建立一个DOM Fragment 对象做为容器
       let content = document.createDocumentFragment()
       for(let count = 0;count<1000;count++){
        //  span此时能够经过DOM API去建立
        let oSpan = document.createElement("span")
        oSpan.innerHTML = "我是一个小测试"
        // 像操做真实DOM同样操做DOM Fragment对象
        content.appendChild(oSpan)
       }
      //  内容处理好了  最后再触发真实的DOM的更改
      container.appendChild(content)

DOM Fragment 对象容许咱们像操做真实 DOM 同样去调用各类各样的 DOM API,咱们的代码质量所以获得了保证。而且它的身份也很是纯粹:当咱们试图将其 append 进真实 DOM 时,它会在乖乖交出自身缓存的全部后代节点后全身而退,完美地完成一个容器的使命,而不会出如今真实的 DOM 结构中。这种结构化、干净利落的特性,使得 DOM Fragment 做为经典的性能优化手段大受欢迎,这一点在 jQuery、Vue 等优秀前端框架的源码中均有体现。

Event Loop 与异步更新策略

Event Loop 中的“渲染时机”

Micro-Task 与 Macro-Task

事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。
常见的 macro-task 好比: setTimeout、setInterval、 setImmediate、script(总体代码)、 I/O 操做、UI 渲染等。
常见的 micro-task 好比: process.nextTick、Promise、MutationObserver 等。

Event Loop 过程解析

一个完整的 Event Loop 过程,能够归纳为如下阶段:
初始状态:调用栈空。micro 队列空,macro 队列里有且只有一个 script 脚本(总体代码)。
全局上下文(script 标签)被推入调用栈,同步代码执行。在执行的过程当中,经过对一些接口的调用,能够产生新的 macro-task 与 micro-task,它们会分别被推入各自的任务队列里。同步代码执行完了,script 脚本会被移出 macro 队列,这个过程本质上是队列的 macro-task 的执行和出队的过程。
上一步咱们出队的是一个 macro-task,这一步咱们处理的是 micro-task。但须要注意的是:当 macro-task 出队时,任务是一个一个执行的;而 micro-task 出队时,任务是一队一队执行的(以下图所示)。所以,咱们处理 micro 队列这一步,会逐个执行队列中的任务并把它出队,直到队列被清空。
在这里插入图片描述

  • 执行渲染操做,更新界面
  • 检查是否存在 Web worker 任务,若是有,则对其进行处理 。

(上述过程循环往复,直到两个队列都清空)

咱们总结一下,每一次循环都是一个这样的过程:
在这里插入图片描述

渲染的时机

假如我想要在异步任务里进行DOM更新,我该把它包装成 micro 仍是 macro 呢?
咱们先假设它是一个 macro 任务,好比我在 script 脚本中用 setTimeout 来处理它:

// task是一个用于修改DOM的回调
setTimeout(task, 0)

如今 task 被推入的 macro 队列。但由于 script 脚本自己是一个 macro 任务,因此本次执行完 script 脚本以后,下一个步骤就要去处理 micro 队列了,再往下就去执行了一次 render,对不对?
但本次render个人目标task其实并无执行,想要修改的DOM也没有修改,所以这一次的render实际上是一次无效的render。

macro 不 ok ,咱们转向 micro 试试看。我用 Promise 来把 task 包装成是一个 micro 任务:

Promise.resolve().then(task)

咱们更新 DOM 的时间点,应该尽量靠近渲染的时机。当咱们须要在异步任务中实现 DOM 修改时,把它包装成 micro 任务是相对明智的选择。

异步更新策略——以 Vue 为例

什么是异步更新?

当咱们使用 Vue 或 React 提供的接口去更新数据时,这个更新并不会当即生效,而是会被推入到一个队列里。待到适当的时机,队列中的更新任务会被批量触发。这就是异步更新。
异步更新能够帮助咱们避免过分渲染,是咱们上节提到的“让 JS 为 DOM 分压”的典范之一。

异步更新的优越性

异步更新的特性在于它只看结果,所以渲染引擎不须要为过程买单。

最典型的例子,好比有时咱们会遇到这样的状况:

// 任务一
this.content = '第一次测试'
// 任务二
this.content = '第二次测试'
// 任务三
this.content = '第三次测试'

咱们在三个更新任务中对同一个状态修改了三次,若是咱们采起传统的同步更新策略,那么就要操做三次 DOM。但本质上须要呈现给用户的目标内容其实只是第三次的结果,也就是说只有第三次的操做是有意义的——咱们白白浪费了两次计算。

但若是咱们把这三个任务塞进异步更新队列里,它们会先在 JS 的层面上被批量执行完毕。当流程走到渲染这一步时,它仅仅须要针对有意义的计算结果操做一次 DOM——这就是异步更新的妙处。

Vue状态更新手法:nextTick

Vue 每次想要更新一个状态的时候,会先把它这个更新操做给包装成一个异步操做派发出去。这件事情,在源码中是由一个叫作 nextTick 的函数来完成的:

export function nextTick(cb?:Function, ctx?:Object){
    let _resolve
    callbacks.push(()=>{
        if(cb){
            try{
                cb.call(ctx)
            }catch(e){
                handleError(e,ctx,'nextTick')
            }
        }else if(_resolve){
            _resolve(ctx)
        }
    })
    // 检查上一个异步任务队列(即名为callbacks的任务数组)是否派发和执行完毕了 pending此处至关于一个锁
    if(!pending){
        // 若上一个异步任务队列已经执行完毕  则将pending设为true(把锁锁上)
        pending = true
        // 是否要求必定要派发为macro任务
        if(useMacroTask){
            macroTimerFunc()
        }else{
            // 若是不说明必定要marco  大家就全都是micro
            microTimerFunc()
        }
    }
    // $flow-disable-line
    if(!cb && typeof Promise !== 'undefined'){
        return new Promise(resolve => {
            _resolve = resolve
        })
    }
}

Vue 的异步任务默认状况下都是用 Promise 来包装的,也就是是说它们都是 micro-task。这一点和咱们“前置知识”中的渲染时机的分析不谋而合。
细化解析一下 macroTimeFunc() 和 microTimeFunc() 两个方法。

macroTimeFunc() 是这么实现的:

// macro首选setImmediate 这个兼容性最差
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
    isNative(MessageChannel) ||
    // PhantomJS
    MessageChannel.toString() === '[object MessageChannelConstructor]'
  )) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  // 兼容性最好的派发方式是setTimeout
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

microTimeFunc() 是这么实现的:

// 简单粗暴  不是ios全都给我去Promise  若是不兼容promise  那么你只能将就一下变成marco了
        if(typeof Promise !== 'undefined'&& isNative(Promise)){
          const p = Promise.resolve()
          microTimerFunc=()=>{
            p.then(flushCallbacks)
            if(isIOS)setTimeout(noop)
          }
        }else{
          // 若是没法派发micro  就退而求次派发为macro
          microTimerFunc = macroTimerFunc
        }

咱们注意到,不管是派发 macro 任务仍是派发 micro 任务,派发的任务对象都是一个叫作 flushCallbacks 的东西,这个东西作了什么呢?

flushCallbacks 源码以下:

function flushCallbacks(){
          pending = false
          //callbacks在nextick中出现过  它是任务数组(队列)
          const copies = callbacks.slice(0)
          callbacks.length = 0
          //将callback中的任务逐个取出执行
          for(let i =0;i<copies.length;i++){
            copies[i]()
          }
        }

Vue 中每产生一个状态更新任务,它就会被塞进一个叫 callbacks 的数组(此处是任务队列的实现形式)中。这个任务队列在被丢进 micro 或 macro 队列以前,会先去检查当前是否有异步更新任务正在执行(即检查 pending 锁)。若是确认 pending 锁是开着的(false),就把它设置为锁上(true),而后对当前 callbacks 数组的任务进行派发(丢进 micro 或 macro 队列)和执行。设置 pending 锁的意义在于保证状态更新任务的有序进行,避免发生混乱

回流(Reflow)与重绘(Repaint)

回流:当咱们对 DOM 的修改引起了 DOM 几何尺寸的变化(好比修改元素的宽、高或隐藏元素等)时,浏览器须要从新计算元素的几何属性(其余元素的几何属性和位置也会所以受到影响),而后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。

重绘:当咱们对 DOM 的修改致使了样式的变化、却并未影响其几何属性(好比修改了颜色或背景色)时,浏览器不需从新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。这个过程叫作重绘。

重绘不必定致使回流,回流必定会致使重绘。硬要比较的话,回流比重绘作的事情更多,带来的开销也更大

回流的“导火索”

最“贵”的操做:改变 DOM 元素的几何属性

这个改变几乎能够说是“牵一发动全身”——当一个DOM元素的几何属性发生变化时,全部和它相关的节点(好比父子节点、兄弟节点等)的几何属性都须要进行从新计算,它会带来巨大的计算量。
常见的几何属性有 width、height、padding、margin、left、top、border 等等
“价格适中”的操做:改变 DOM 树的结构
这里主要指的是节点的增减、移动等操做。浏览器引擎布局的过程,顺序上能够类比于树的前序遍历——它是一个从上到下、从左到右的过程。一般在这个过程当中,当前元素不会再影响其前面已经遍历过的元素。

最容易被忽略的操做:获取一些特定属性的值
当你要用到像这样的属性:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight 时,你就要注意了!
“像这样”的属性,究竟是像什么样?——这些值有一个共性,就是须要经过即时计算获得。所以浏览器为了获取这些值,也会进行回流。
除此以外,当咱们调用了 getComputedStyle 方法,或者 IE 里的 currentStyle 时,也会触发回流。原理是同样的,都为求一个“即时性”和“准确性”。

如何规避回流与重绘

将“导火索”缓存起来,避免频繁改动

有时咱们想要经过屡次计算获得一个元素的布局位置,咱们可能会这样作:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<style>
    #el{
        width:100%;
        height:100%;
        background-color: yellow;
        position:absolute;
    }
</style>
<body>
    <div id="el"></div>
    <script>
        // 获取el元素
        const el = document.getElementById('el')
        // 这里循环断定比较简单实际中获取会拓展出比较复杂的断定需求
        for(let i =0;i<10;i++){
            el.style.top = el.offsetHeight + 10 + 'px'
            el.style.left = el.offsetLeft + 10 + 'px'
        }
    </script>
</body>
</html>

这样作,每次循环都须要获取屡次“敏感属性”,是比较糟糕的。咱们能够将其以 JS 变量的形式缓存起来,待计算完毕再提交给浏览器发出重计算请求:

//    缓存offsetLeft与offsetTop的值
    const el = document.getElementById('el')
    let offLeft = el.offsetLeft,offTop = el.offsetTop
    // 在js层面进行计算
    for(let i =0;i<10;i++){
        offsetLeft += 10
        offsetTop += 10
    }
    // 一次性将计算结果应用到DOM上
    el.style.left = offLeft + 'px'
    el.style.top = offTop + 'px'

避免逐条改变样式,使用类名去合并样式

好比咱们能够把这段单纯的代码

const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'

优化成一个有 class 加持的样子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<style>
   .basic_style {
       width: 100px;
       height: 200px;
       border: 10px solid red;
       color:red;
   }
</style>
<body>
    <div id="container"></div>
    <script>
      let container = document.getElementById('container')
      container.classList.add('basic_style')
    </script>
</body>
</html>

前者每次单独操做,都去触发一次渲染树更改,从而致使相应的回流与重绘过程。

合并以后,等于咱们将全部的更改一次性发出,用一个 style 请求解决掉了。

将 DOM “离线”

所说的回流和重绘,都是在“该元素位于页面上”的前提下会发生的。一旦咱们给元素设置 display: none,将其从页面上“拿掉”,那么咱们的后续操做,将没法触发回流与重绘——这个将元素“拿掉”的操做,就叫作 DOM 离线化。

仍以这段代码片断为例:

const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'

离线化后就是这样:

const container = document.getElementById('container')
container.style.display = 'none'
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
.....
container.style.display = 'block'

把它拿下来了,后续无论我操做这个元素多少次,每一步的操做成本都会很是低。当咱们只须要进行不多的 DOM 操做时,DOM 离线化的优越性确实不太明显。一旦操做频繁起来,这“拿掉”和“放回”的开销都将会是很是值得的。

Flush 队列:浏览器并无那么简单

let container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'

这段代码里,浏览器进行了多少次的回流或重绘呢
“width、height、border是几何属性,各触发一次回流;color只形成外观的变化,会触发一次重绘。”
由于现代浏览器是很聪明的。浏览器本身也清楚,若是每次 DOM 操做都即时地反馈一次回流或重绘,那么性能上来讲是扛不住的。因而它本身缓存了一个 flush 队列,把咱们触发的回流与重绘任务都塞进去,待到队列里的任务多起来、或者达到了必定的时间间隔,或者“不得已”的时候,再将这些任务一口气出队。所以咱们看到,上面就算咱们进行了 4 次 DOM 更改,也只触发了一次 Layout 和一次 Paint。

提到过有一类属性很特别,它们有很强的“即时性”。当咱们访问这些属性时,浏览器会为了得到此时此刻的、最准确的属性值,而提早将 flush 队列的任务出队——这就是所谓的“不得已”时刻。

并非全部的浏览器都是聪明的。Chrome 里行得通的东西,到了别处(好比 IE)就不必定行得通了。而咱们并不知道用户会使用什么样的浏览器。若是不手动作优化,那么一个页面在不一样的环境下就会呈现不一样的性能效果,这对咱们、对用户都是不利的。所以,养成良好的编码习惯、从根源上解决问题,仍然是最周全的方法。

优化首屏体验——Lazy-Load

Lazy-Load,翻译过来是“懒加载”。它是针对图片加载时机的优化:在一些图片量比较大的网站,
,若是咱们尝试在用户打开页面的时候,就把全部的图片资源加载完毕,那么极可能会形成白屏、卡顿等现象,由于图片真的太多了,一口气处理这么多任务,浏览器作不到啊!

只要咱们能够在页面打开的时候把首屏的图片资源加载出来,用户就会认为页面是没问题的。至于下面的图片,咱们彻底能够等用户下拉的瞬间再即时去请求、即时呈现给他。这样一来,性能的压力小了,用户的体验却没有变差——这个延迟加载的过程,就是 Lazy-Load。

写一个 Lazy-Load

Lazy-Load 的思路及实现方式为大厂面试常考题
咱们在 index.html 中,为这些图片预置 img 标签:

<!--
 * @Author: yang
 * @Date: 2020-11-29 15:02:59
 * @LastEditors: yang
 * @LastEditTime: 2020-11-29 15:28:43
 * @FilePath: \gloud-h5-demo\src\component\index\index.html
-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .img{
            width: 200px;
            height: 200px;
            background-color: gray;
        }
    </style>
</head>
<body>
    <div class="img">
        <img class="pic" alt="加载中" data-src="./images/1.png">
    </div>
    <div class="img">
        <img class="pic" alt="加载中" data-src="./images/2.png">
    </div>
    <div class="img">
        <img class="pic" alt="加载中" data-src="./images/3.png">
    </div>
    <div class="img">
        <img class="pic" alt="加载中" data-src="./images/4.png">
    </div>
    <div class="img">
        <img class="pic" alt="加载中" data-src="./images/5.png">
    </div>
    <div class="img">
        <img class="pic" alt="加载中" data-src="./images/6.png">
    </div>
    <div class="img">
        <img class="pic" alt="加载中" data-src="./images/7.png">
    </div>
    <div class="img">
        <img class="pic" alt="加载中" data-src="./images/8.png">
    </div>
    <div class="img">
        <img class="pic" alt="加载中" data-src="./images/9.png">
    </div>
</body>
</html>

在懒加载的实现中,有两个关键的数值:一个是当前可视区域的高度,另外一个是元素距离可视区域顶部的高度。
当前可视区域的高度, 在和现代浏览器及 IE9 以上的浏览器中,能够用 window.innerHeight 属性获取。在低版本 IE 的标准模式中,能够用 document.documentElement.clientHeight 获取,这里咱们兼容两种状况:

const viewHeight = window.innerHeight||document.documentElement.clientHeight

而元素距离可视区域顶部的高度,咱们这里选用 getBoundingClientRect() 方法来获取返回元素的大小及其相对于视口的位置
(DOMRect 对象包含了一组用于描述边框的只读属性——left、top、right 和 bottom,单位为像素。除了 width 和 height 外的属性都是相对于视口的左上角位置而言的。)

能够看出,top 属性表明了元素距离可视区域顶部的高度,正好能够为咱们所用

Lazy-Load 方法开工

// 获取全部的图片标签
        const imgs = document.getElementsByTagName('img')
        // 获取可视区域的高度
        const viewHeight = window.innerHeight || document.documentElement.clientHeight
        // num用于统计当前显示到了哪一张图片  避免每次都从第一张图片开始检查是否漏出
        let num = 0
        function lazyload(){
            for(let i = num;i<imgs.length;i++){
                // 用可视区域高度减去元素顶部据可视区域顶部的高度
            let distance = viewHeight - imgs[i].getBoundingClientRect().top
            // 若是可视区域高度大于元素顶部距离可视区域顶部的高度,说明元素露出
            if(distance>=0){
                //给元素写入真实的src  展现图片
                imgs[i].src = imgs[i].getAttribute('data-src')
                // 前i张图片已经加载完毕  下次从i+1张开始检查是否露出
                num = i+1
            }
                
            }
        }
        // 监听Scroll事件
        window.addEventListener('scroll',lazyload,false)

这个 scroll 事件,是一个危险的事件——它太容易被触发了。试想,用户在访问网页的时候,是否是能够无限次地去触发滚动?尤为是一个页面死活加载不出来的时候,疯狂调戏鼠标滚轮(或者浏览器滚动条)的用户可不在少数啊!

再回头看看咱们上面写的代码。按照咱们的逻辑,用户的每一次滚动都将触发咱们的监听函数。函数执行是吃性能的,频繁地响应某个事件将形成大量没必要要的页面计算。所以,咱们须要针对那些有可能被频繁触发的事件做进一步地优化。

事件的节流(throttle)与防抖(debounce)

scroll 事件是一个很是容易被反复触发的事件。其实不止 scroll 事件,resize 事件、鼠标事件(好比 mousemove、mouseover 等)、键盘事件(keyup、keydown 等)都存在被频繁触发的风险。

频繁触发回调致使的大量计算会引起页面的抖动甚至卡顿。为了规避这种状况,咱们须要一些手段来控制事件被触发的频率。就是在这样的背景下,throttle(事件节流)和 debounce(事件防抖)出现了。

“节流”与“防抖”的本质

这两个东西都以闭包的形式存在。

它们经过对事件对应的回调函数进行包裹、以自由变量的形式缓存时间信息,最后用 setTimeout 来控制事件的触发频率。

Throttle: 第一我的说了算

throttle 的中心思想在于:在某段时间内,无论你触发了多少次回调,我都只认第一次,并在计时结束时给予响应。
所谓的“节流”,是经过在一段时间内无视后来产生的回调请求来实现的。
每当用户触发了一次 scroll 事件,咱们就为这个触发操做开启计时器。一段时间内,后续全部的 scroll 事件都会被看成“一辆车的乘客”——它们没法触发新的 scroll 回调。直到“一段时间”到了,第一次触发的 scroll 事件对应的回调才会执行,而“一段时间内”触发的后续的 scroll 回调都会被节流阀无视掉。

//  fn是咱们须要包装的事件回调  interval是时间间隔的阈值
        function throttle(fn,interval){
            // last为上一次触发回调的时间、
            let last = 0
            // 将throwttle处理结果当作函数返回
            return function(){
                // 保留调用时的this上下文
                let context = this
                // 保留调用时传入的参数
                let args = arguments
                //记录本次触发回调的时间
                let now = + new Date()
                //判断上次触发的时间和本次触发的时间差是否小于时间间隔的阀值
                if(now - last >= interval){
                    // 若是时间间隔大于咱们设定的时间间隔阀值  则执行回调
                    last = now
                    fn.apply(context,args)
                }
            }
        }
        // 用throwttle来包装scroll的回调
        document.addEventListener('scroll',throttle(()=>console.log('触发了滚动事件'),1000))

Debounce: 最后一我的说了算

防抖的中心思想在于:我会等你到底。在某段时间内,无论你触发了多少次回调,我都只认最后一次。

//   fn是须要包装的事件回调  delay是每次推迟执行的等待时间
    function debounce(fn,delay){
        //定时器
        let timer = null
        // 将debounce处理结果当作函数返回
        return function(){
            //保留调用时的this上下文
            let context = this
            // 保留调用时传入的参数
            let args = arguments
            //每次事件被触发时  都去清除以前的旧定时器
            if(timer){
                clearTimeout(timer)
            }
            // 设立新定时器
            timer = setTimeout(function(){
                fn.apply(context,args)
            },delay)
        }
    }
    document.addEventListener('scroll',debounce(()=>console.log('触发了滚动事件'),1000))

用 Throttle 来优化 Debounce

debounce 的问题在于它“太有耐心了”。试想,若是用户的操做十分频繁——他每次都不等 debounce 设置的 delay 时间结束就进行下一次操做,因而每次 debounce 都为该用户从新生成定时器,回调函数被延迟了不可胜数次。频繁的延迟会致使用户迟迟得不到响应,用户一样会产生“这个页面卡死了”的观感。

为了不弄巧成拙,咱们须要借力 throttle 的思想,打造一个“有底线”的 debounce——等你能够,但我有个人原则:delay 时间内,我能够为你从新生成定时器;但只要delay的时间到了,我必需要给用户一个响应。这个 throttle 与 debounce “合体”思路,已经被不少成熟的前端库应用到了它们的增强版 throttle 函数的实现中:

//  fn是咱们须要包装的事件回调, delay是时间间隔的阀值
    function throttle(fn,delay){
        // last 为上次触发回调的事件 timer是定时器
        let last = 0,timer = null;
        // 将throttle处理结果当作函数返回
        return function(){
            // 保留调用时的this上下文
            let context = this
            // 保留调用时传入的参数
            let args = arguments
            // 记录本次触发回调的时间
            let now = +new Date()
            // 判断上次触发时间和本地触发的时间差是否小于时间间隔的阀值
            if(now-last>delay){
                // 若是时间间隔小于咱们设定的时间间隔阀值  则为本次触发操做设立一个新的定时器
                clearTimeout(timer)
                timer = setTimeout(function(){
                    last = now
                    fn.apply(contxt,args)
                },delay)
            }else{
                // 若是时间间隔超出了咱们设定的时间间隔阀值  那就不等了  不管如何要反馈给用户一次响应
                last = now
                fn.apply(context,args)
            }
        }
    }
    // 用新的throttle包装scroll的回调
    document.addEventListener('scroll',throttle(()=>console.log('触发了滚动事件'),1000))

性能监测

可视化监测:从 Performance 面板提及

Performance是Chrome
提供给咱们的开发者工具,用于记录和分析咱们的应用在运行时的全部活动。它呈现的数据具备实时性、多维度的特色,能够帮助咱们很好地定位性能问题。
开始记录
右键打开开发者工具,选中咱们的
Performance
面板:
在这里插入图片描述
当咱们选中图中所标示的实心圆按钮,Performance

会开始帮咱们记录咱们后续的交互操做;当咱们选中圆箭头按钮,Performance
会将页面从新加载,计算加载过程当中的性能表现。
tips:使用
Performance
工具时,为了规避其它
Chrome
插件对页面的性能影响,咱们最好在无痕模式下打开页面

挖掘性能瓶颈

看 Main 栏目下的火焰图和 Summary 提供给咱们的饼图——这二者和概述面板中的 CPU 一栏结合,能够帮咱们迅速定位性能瓶颈
在这里插入图片描述
从上到下,依次为概述面板、详情面板
观察一下概述面板
FPS:这是一个和动画性能密切相关的指标,它表示每一秒的帧数。图中绿色柱状越高表示帧率越高,体验就越流畅。若出现红色块,则表明长时间帧,极可能会出现卡顿。图中以绿色为主,偶尔出现红块,说明网页性能并不糟糕,但仍有可优化的空间。

CPU:表示CPU的使用状况,不一样的颜色片断表明着消耗CPU资源的不一样事件类型。这部分的图像和下文详情面板中的Summary内容有对应关系,咱们能够结合这二者挖掘性能瓶颈。

NET:粗略的展现了各请求的耗时与先后顺序。这个指标通常来讲帮助不大。
先看 CPU 图表和 Summary 饼图。CPU 图表中,咱们能够根据颜色填充的饱满程度,肯定 CPU 的忙闲,进而了解该页面的总的任务量。而 Summary 饼图则以一种直观的方式告诉了咱们,哪一个类型的任务最耗时(从本例来看是脚本执行过程)。这样咱们在优化的时候,就能够抓到“主要矛盾”,进而有的放矢地开展后续的工做了。

再看 Main 提供给咱们的火焰图。这个火焰图很是关键,它展现了整个运行时主进程所作的每一件事情(包括加载、脚本运行、渲染、布局、绘制等)。x 轴表示随时间的记录。每一个长条就表明一个活动。更宽的条形意味着事件须要更长时间。y 轴表示调用堆栈,咱们能够看到事件是相互堆叠的,上层的事件触发了下层的事件。

CPU 图标和 Summary 图都是按照“类型”给咱们提供性能信息,而 Main 火焰图则将粒度细化到了每个函数的调用。究竟是从哪一个过程开始出问题、是哪一个函数拖了后腿、又是哪一个事件触发了这个函数,这些具体的、细致的问题都将在 Main 火焰图中获得解答。

可视化监测: 更加聪明的 LightHouse

Performance 无疑能够为咱们提供不少有价值的信息,但它的展现做用大于分析做用。它要求使用者对工具自己及其所展现的信息有充分的理解,可以将晦涩的数据“翻译”成具体的性能问题。

程序员们许了个愿:若是工具能帮助咱们把页面的问题也分析出来就行了!上帝听到了这个愿望,因而给了咱们 LightHouse:
Lighthouse 是一个开源的自动化工具,用于改进网络应用的质量。 你能够将其做为一个 Chrome 扩展程序运行,或从命令行运行。 为Lighthouse 提供一个须要审查的网址,它将针对此页面运行一连串的测试,而后生成一个有关页面性能的报告。
首先在 Chrome 的应用商店里下载一个 LightHouse。这一步 OK 以后,咱们浏览器右上角会出现一个小小的灯塔 ICON。打开咱们须要测试的那个页面,点击这个 ICON,唤起以下的面板:
在这里插入图片描述
而后点击“Generate report”按钮,只需静候数秒,LightHouse 就会为咱们输出一个完美的性能报告。

这里我拿掘金小册首页“开刀”:
稍事片刻,Report 便输出成功了,LightHouse 默认会帮咱们打开一个新的标签页来展现报告内容。报告内容很是丰富,首先咱们看到的是总体的跑分状况:
在这里插入图片描述
上述分别是页面性能、PWA(渐进式 Web 应用)、可访问性(无障碍)、最佳实践、SEO 五项指标的跑分。孰强孰弱,咱们一看便知。

向下拉动 Report 页,咱们还能够看到每个指标的细化评估:
在这里插入图片描述
在“Opportunities”中,LightHouse 甚至针对咱们的性能问题给出了可行的建议、以及每一项优化操做预期会帮咱们节省的时间。这份报告的可操做性是很强的——咱们只须要对着 LightHouse 给出的建议,一条一条地去尝试,就能够看到本身的页面,在一秒一秒地变快。

除了直接下载,咱们还能够经过命令行使用 LightHouse:

npm install -g lighthouse
    lighthouse https://juejin.im/books

一样能够获得掘金小册的性能报告。

此外,从 Chrome 60 开始,DevTools 中直接加入了基于 LightHouse 的 Audits 面板:
在这里插入图片描述

可编程的性能上报方案: W3C 性能 API Performance

W3C 规范为咱们提供了 Performance 相关的接口。它容许咱们获取到用户访问一个页面的每一个阶段的精确时间,从而对性能进行分析。咱们能够将其理解为 Performance 面板的进一步细化与可编程化。

当下的前端世界里,数据可视化的概念已经被炒得很是热了,Performance 面板就是数据可视化的典范。那么为何要把已经可视化的数据再掏出来处理一遍呢?这是由于,须要这些数据的人不止咱们前端——不少状况下,后端也须要咱们提供性能信息的上报。此外,Performance 提供的可视化结果并不必定可以知足咱们实际的业务需求,只有拿到了真实的数据,咱们才能够对它进行二次处理,去作一个更加深层次的可视化。

在这种需求背景下,咱们就不得不祭出 Performance API了。
访问 performance 对象

performance 是一个全局对象。咱们在控制台里输入 window.performance,就可一窥其全貌:

在这里插入图片描述
关键时间节点

在 performance 的 timing 属性中,咱们能够查看到以下的时间戳:
在这里插入图片描述
这些时间戳与页面整个加载流程中的关键时间节点有着一一对应的关系
在这里插入图片描述
经过求两个时间点之间的差值,咱们能够得出某个过程花费的时间,举个🌰:

const timing = window.performance.timing
// DNS查询耗时
timing.domainLookupEnd - timing.domainLookupStart
  
// TCP链接耗时
timing.connectEnd - timing.connectStart
 
// 内容加载耗时
timing.responseEnd - timing.requestStart

···

除了这些常见的耗时状况,咱们更应该去关注一些关键性能指标:firstbyte、fpt、tti、ready 和 load 时间。这些指标数据与真实的用户体验息息相关,是咱们平常业务性能监测中不可或缺的一部分:

// firstbyte:首包时间    
timing.responseStart – timing.domainLookupStart    

// fpt:First Paint Time, 首次渲染时间 / 白屏时间
timing.responseEnd – timing.fetchStart

// tti:Time to Interact,首次可交互时间    
timing.domInteractive – timing.fetchStart

// ready:HTML 加载完成时间,即 DOM 就位的时间
timing.domContentLoaded – timing.fetchStart

// load:页面彻底加载时间
timing.loadEventStart – timing.fetchStart

以上这些经过 Performance API 获取到的时间信息都具备较高的准确度。咱们能够对此进行一番格式处理以后上报给服务端,也能够基于此去制做相应的统计图表,从而实现更加精准、更加个性化的性能耗时统计。

此外,经过访问 performance 的 memory 属性,咱们还能够获取到内存占用相关的数据;经过对 performance 的其它属性方法的灵活运用,咱们还能够把它耦合进业务里,实现更加多样化的性能监测需求——灵活,是可编程化方案最大的优势。

相关文章
相关标签/搜索