前端性能优化

你们是从何时接触性能优化的呢?javascript

第一时间想到的又是什么呢?雅虎军规 ? 高性能javascript ?css

性能优化没有标准答案,咱们只能不断地把从搜索引擎和书中的知识付诸实践,这个过程是漫长且艰辛的html

本文为总结记录学习修言大佬小册,感兴趣的同窗能够购买支持正版前端

一切从一道面试题开始

从输入 URL 到页面加载完成,发生了什么?java

归纳来讲,分为5步node

  1. DNS把url解析成IP
  2. 客户端经过Ip和服务端创建tcp链接
  3. 客户端发起http请求
  4. 服务端处理http请求并返回响应
  5. 客户端拿到响应并渲染页面

每一步均可以说的很细,这5个过程就是提升前端性能的根本的切入点react

关于第一第二步,咱们前端能作的很是有限,理解为主,webpack

DNS 解析花时间,能不能尽可能减小解析次数或者把解析前置?能——浏览器 DNS 缓存和 DNS prefetchgit

TCP 每次的三次握手都急死人,有没有解决方案?有——长链接、预链接、接入 SPDY 协议es6

为了知其因此然(应对深究的面试官),咱们了解下DNS的解析过程:

  1. 浏览器先检查自身缓存中有没有被解析过的这个域名对应的ip地址,若是有,解析结束
  2. 若是浏览器缓存没有命中,浏览器会检查操做系统缓存中有没有对应的已解析过的结果。而操做系统也有一个域名解析的过程。在windows中可经过c盘里一个叫hosts的文件来设置,若是你在这里指定了一个域名对应的ip地址,那浏览器会首先使用这个ip地址。可是这种操做系统级别的域名解析规程也被不少黑客利用,经过修改你的hosts文件里的内容把特定的域名解析到他指定的ip地址上,形成所谓的域名劫持。因此在windows7中将hosts文件设置成了readonly,防止被恶意篡改。
  3. 若是至此尚未命中域名,才会真正的请求本地域名服务器(LDNS)来解析这个域名,这台服务器通常在你的城市的某个角落,距离你不会很远,而且这台服务器的性能都很好,通常都会缓存域名解析结果,大约80%的域名解析到这里就完成了。
  4. 若是LDNS仍然没有命中,就直接跳到Root Server 域名服务器请求解析
  5. 根域名服务器返回给LDNS一个所查询域的主域名服务器(gTLD Server,国际顶尖域名服务器,如.com .cn .org等)地址
  6. 此时LDNS再发送请求给上一步返回的gTLD
  7. 接受请求的gTLD查找并返回这个域名对应的Name Server的地址,这个Name Server就是网站注册的域名服务器
  8. Name Server根据映射关系表找到目标ip,返回给LDNS
  9. LDNS缓存这个域名和对应的ip
  10. LDNS把解析的结果返回给用户,用户根据TTL值缓存到本地系统缓存中,域名解析过程至此结束

HTTP 链接这一层面的优化才是咱们网络优化的核心,

  1. 减小http请求次数
  2. 减小单次请求花费的时间

减小h t tp请求次数就一定会增长单次请求的开销,两点怎么权衡?

网络篇

减小单次请求花费的时间

webpack 性能优化

最多见操做就是资源的压缩和合并,该操做最多见的工具就是webpack, 因此问题就指向了webpack的性能瓶颈

构建过程时间太长

打包体积太大

从 webpack v4.0.0 开始,能够不用引入一个配置文件。然而,webpack 仍然仍是高度可配置的

不要让loader作太多事

以 babel-loader 为例:

下面直接贴webpack官网的描述

babel-loader 很慢!

确保转译尽量少的文件。你可能使用 /\.js$/ 来匹配,这样也许会去转译 node_modules 目录或者其余不须要的源代码。

要排除 node_modules,参考文档中的 loaders 配置的 exclude 选项。

你也能够经过使用 cacheDirectory 选项,将 babel-loader 提速至少两倍。 这会将转译的结果缓存到文件系统中。

babel 对一些公共方法使用了很是小的辅助代码,好比 _extend。 默认状况下会被添加到每个须要它的文件中

你能够引入 babel runtime 做为一个独立模块,来避免重复引入。

下面的配置禁用了 babel 自动对每一个文件的 runtime 注入,而是引入 babel-plugin-transform-runtime 而且使全部辅助代码从这里引用。

rules: [
  // 'transform-runtime' 插件告诉 babel 要引用 runtime 来代替注入。
  {
    test: /\.js$/,
    exclude: /(node_modules|bower_components)/,
    use: {
      loader: 'babel-loader?cacheDirectory=true',
      options: {
        presets: ['@babel/preset-env'],
        plugins: ['@babel/transform-runtime']
      }
    }
  }
]

不要放过第三方库

打包第三方依赖推荐 DllPlugin

DllPlugin 是基于 Windows 动态连接库(dll)的思想被创做出来的。这个插件会把第三方库单独打包到一个文件中,这个文件就是一个单纯的依赖库。这个依赖库不会跟着你的业务代码一块儿被从新打包,只有当依赖自身发生版本变化时才会从新打包

Dll.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 文件夹里会出现这样两个文件:

  1. vendor-manifest.json: 用于描述每一个第三方库对应的具体路径
  2. vendor.js: 咱们第三方库打包的结果

Webpack.config.js

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'),
    })
  ]
}

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

webpack因为是node编写的,node是单线程的,就算此刻存在多个任务,你也只能排队一个接一个地等待处理, 为了充分利用多核cpu的资源,根据cpu的核数,咱们能够fork多个进程, 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

你们能够点进去看一下,它将建立一个交互式treemap可视化你的包的内容,这样你就能够知道哪些包是引发你打包体积过大的罪魁祸首

删除冗余代码

tree shaking

tree shaking 是一个术语,一般用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 importexport。这个术语和概念其实是兴起于 ES2015 模块打包工具 rollup

新的 webpack 4 正式版本,扩展了这个检测能力,经过 package.json"sideEffects" 属性做为标记,向 compiler 提供提示,代表项目中的哪些文件是 "pure(纯的 ES2015 模块)",由此能够安全地删除文件中未使用的部分。

若是全部代码都不包含反作用,咱们就能够简单地将该属性标记为 false,来告知 webpack,它能够安全地删除未用到的 export 导出。

{
  "name": "your-project",
  "sideEffects": false
}

若是你的代码确实有一些反作用,那么能够改成提供一个数组:

数组方式支持相关文件的相对路径、绝对路径和 glob 模式。它在内部使用 micromatch

注意,任何导入的文件都会受到 tree shaking 的影响。这意味着,若是在项目中使用相似 css-loader 并导入 CSS 文件,则须要将其添加到 side effect 列表中,以避免在生产模式中无心中将它删除:

{
  "name": "your-project",
  "sideEffects": [
    "./src/some-side-effectful-file.js"
    "*.css"
  ]
}

Tree-Shaking 的针对性很强,它更适合用来处理模块级别的冗余代码

UglifyJsPlugin

// 在压缩过程当中对碎片化的冗余代码(如 console 语句、注释等)进行自动化删除
const webpack = require('webpack');
module.exports = {
 plugins: [
   new webpack.optimize.UglifyJsPlugin({
   		// 容许并发
     parallel: true,
     // 开启缓存
     cache: true,
     compress: {
       // 删除全部的console语句    
       drop_console: true,
       // 把使用屡次的静态值自动定义为变量
       reduce_vars: true,
     },
     output: {
       // 不保留注释
       comment: false,
       // 使输出的代码尽量紧凑
       beautify: false
     }
  }),
 ]
}

按需加载

好比路由的懒加载,tab组件的懒加载

Gzip

gzip的基础是DEFLATE,DEFLATE是LZ77与哈夫曼编码的一个组合体。DEFLATE最初是做为LZW以及其它受专利保护的数据压缩算法的替代版本而设计的,当时那些专利限制了compress以及其它一些流行的归档工具的应用

开启gzip压缩,只须要在请求头中加上

accept-encoding:gzip

压缩后一般能帮咱们减小响应 70% 左右的大小

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

图片优化

图片优化就是在图片体积和图片质量之间作权衡

前置知识: 在计算机中,像素用二进制数来表示, n位二进制能够表示2^n种颜色,

因此二进制位数越多,可表示的颜色种类就越多,成像效果越细腻,图片体积越大

咱们须要在不一样的业务场景选择合适的图片类型

JPEG/JPG

体积小有损压缩不支持透明加载快

当咱们把图片体积压缩至原有体积的 50% 如下时,JPG 仍然能够保持住 60% 的品质。此外,JPG 格式以 24 位存储单个图,能够呈现多达2^24 = 16,777,216 种颜色,足以应对大多数场景下对色彩的要求

使用场景

JPG 图片常常做为大的背景图、轮播图或 Banner 图出现,使用 JPG 呈现大图,既能够保住图片的质量,又不会带来使人头疼的图片体积,是当下比较推崇的一种方案。

PNG-8/PNG-24

体积大无损压缩支持透明质量高

8和24表明2进制位数

Png-8: 2^8 = 256中颜色

Png-24: 2^24 = 16,777,216 种颜色

追求极致的显示效果,不在乎图片大小的能够选择png-24

实践中,若是png-8没有带来视觉可辨别的色彩缺陷,考虑到体积,通常使用png-8

使用场景

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

性能方面堪称业界楷模的淘宝首页页面上的 Logo,不管大小,都是 PNG 格式

SVG

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

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

优点

  • SVG 与 PNG 和 JPG 相比,文件体积更小,可压缩性更强
  • 做为矢量图,它最显著的优点在于图片可无限放大而不失真, 这使得 SVG 即便是被放到视网膜屏幕上,也能够一如既往地展示出较好的成像品质——1 张 SVG 足以适配 n 种分辨率
  • SVG 是文本文件。咱们既能够像写代码同样定义 SVG,把它写在 HTML 里、成为 DOM 的一部分,也能够把对图形的描述写入以 .svg 为后缀的独立文件, 这使得 SVG 文件能够被很是多的工具读取和修改,具备较强的灵活性

劣势

  • 渲染成本高
  • 相比其余图片格式,学习成本高,由于它是可编程的

使用场景

小图标

Base64

文本文件依赖编码小图标解决方案

Base64 并不是一种图片格式,而是一种编码方式。Base64 和雪碧图同样,是做为小图标解决方案而存在的。在了解 Base64 以前,咱们先来了解一下雪碧图

雪碧图

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

前端使用background-position 来获取不一样位置的图标

Base64 图片的出现,也是为了减小加载网页图片时对服务器的请求次数,从而提高网页性能。Base64 是做为雪碧图的补充而存在的。

使用场景

小logo, 小icon

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

劣势

Base64 编码后,图片大小会膨胀为原文件的 4/3(这是由 Base64 的编码原理决定的),

若是咱们把大图也编码到 HTML 或 CSS 文件中,后者的体积会明显增长,即使咱们减小了 HTTP 请求,也没法弥补这庞大的体积带来的性能开销,得不偿失

base64编码工具推荐

webpack 的 url-loader , limit参数表示 指定文件的最大大小,以字节为单位,只有8192byte一下大小的图片才会进行base64编码

module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
            },
          },
        ],
      },
    ],
  },
};

WebP

年轻的全能型选手

它于 2010 年被提出, 是 Google 专为 Web 开发的一种旨在加快图片加载速度的图片格式,它支持有损压缩和无损压缩

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

  • 最大的缺陷就是兼容性
  • WebP 还会增长服务器的负担——和编码 JPG 文件相比,编码一样质量的 WebP 文件会占用更多的计算资源

应用场景

要使用webP咱们就必须为不兼容的浏览器进行降级处理

咱们能够看看淘宝是怎么作的

在谷歌浏览器打开淘宝,打开控制台,搜索.webp

其中一个img的src是这样的

img.alicdn.com/imgextra/i3/6000000002336/O1CN019A9rll1T7vqVs4Wur_!!6000000002336-0-octopus.jpg_400x400q90.jpg_.webp

在safari中打开淘宝,打开控制台,查看同一张图片的src

img.alicdn.com/imgextra/i3/6000000002336/O1CN019A9rll1T7vqVs4Wur_!!6000000002336-0-octopus.jpg_400x400q90.jpg

淘宝是会根据浏览器的型号来判断是否支持webp,不支持就把.webp后缀切换成.jpg

更灵活的方案

把判断逻辑交给后端,服务器根据 请求头的 Accept 字段 来判断是否支持webp, 不然返回原图(jpg),

这样作的好处是当webp的兼容性发生变化时,前端不用修改判断是否支持webp的代码

存储篇

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

浏览器缓存机制

按照获取资源时请求的优先级排序:

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

Memory Cache(内存缓存)

是指存在内存中的缓存。从优先级上来讲,它是浏览器最早尝试去命中的一种缓存。从效率上来讲,它是响应速度最快的一种缓存。

内存缓存是快的,也是“短命”的。它和渲染进程“生死相依”,当进程结束后,也就是 tab 关闭之后,内存里的数据也将不复存在

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

Service Worker Cache

Service Worker 是一种独立于主线程以外的 Javascript 线程。它脱离于浏览器窗体,所以没法直接访问 DOM。这样独立的个性使得 Service Worker 的“我的行为”没法干扰页面的性能,这个“幕后工做者”能够帮咱们实现离线缓存消息推送网络代理等功能。咱们借助 Service worker 实现的离线缓存就称为 Service Worker Cache。

Service Worker 的生命周期包括 installactiveworking三个阶段。一旦 Service Worker 被 install,它将始终存在,只会在 active 与 working 之间切换,除非咱们主动终止它。这是它能够用来实现离线存储的重要先决条件。

如何使用 service worker(必须以 https 协议为前提)

新建test.js文件

// Service Worker会监听 install事件,咱们在其对应的回调里能够实现初始化的逻辑  
self.addEventListener('install', event => {
  event.waitUntil(
    // 考虑到缓存也须要更新,open内传入的参数为缓存的版本号
    // caches 为浏览器的API
    // CacheStorage {}
		//   __proto__: CacheStorage
		//   delete: ƒ delete()
		//  	has: ƒ has()
    //   keys: ƒ keys()
    //    match: ƒ match()
    //    open: ƒ open()
    //    constructor: ƒ CacheStorage()
    //   Symbol(Symbol.toStringTag): "CacheStorage"
    //   __proto__: Object
    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(function(cache) {
          cache.put(event.request, response);
        });
        return response.clone();
      });
    })
  );
});

在项目代码入口j s文件中加入

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

HTTP Cache(重点)

HTTP 缓存是咱们平常开发中最为熟悉的一种缓存机制。它又分为强缓存协商缓存。优先级较高的是强缓存,在命中强缓存失败的状况下,才会走协商缓存。

强缓存

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

命中强缓存的状况下,返回的 HTTP 状态码为200 (from disk cache)

Cache-Control 的 max-age 配置项相对于 expires 的优先级更高。当 Cache-Control 与 expires 同时出现时,咱们以 Cache-Control 为准。

Cache-Control
cache-control: max-age=31536000

max-age表示的有效时长, 单位是秒,表示该资源31536000秒内是有效的

public 与 private

public 与 private 是针对资源是否可以被代理服务缓存而存在的一组对立概念。

若是咱们为资源设置了 public,那么它既能够被浏览器缓存,也能够被代理服务器缓存;若是咱们设置了 private,则该资源只能被浏览器缓存。private 为默认值。但多数状况下,public 并不须要咱们手动设置

no-store与no-cache

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

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

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

expires 设置的是资源到期时间(服务器时间)

接下来若是咱们试图再次向服务器请求资源,浏览器就会先对比本地时间和 expires 的时间戳,若是本地时间小于 expires 设定的过时时间,那么就直接去缓存中取这个资源。因为时间戳是服务器来定义的,而本地时间的取值却来自客户端,所以 expires 的工做机制对客户端时间与服务器时间之间的一致性提出了极高的要求,若服务器与客户端存在时差,将带来意料以外的结果。

协商缓存

协商缓存机制下,浏览器须要向服务器去询问缓存的相关信息,进而判断是从新发起请求、下载完整的响应,仍是从本地获取缓存的资源。

若是服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种状况下网络请求对应的状态码是 304

Etag 在感知文件变化上比 Last-Modified 更加准确,优先级也更高。当 Etag 和 Last-Modified 同时存在时,以 Etag 为准.

Last-Modified

首次请求时,响应头里会携带Last-Modified字段,像这样

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

再次请求相同的资源,请求头里会带上If-Modified-Since字段,

值是上一次请求响应头的Last-Modified的值

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

服务器接收到这个时间戳后,会比对该时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化。若是发生了变化,就会返回一个完整的响应内容,并在 响应头 中添加新的 Last-Modified 值;不然,返回如上图的 304 响应,响应头 不会再添加 Last-Modified 字段。

弊端

  • 咱们编辑了文件,但文件的内容没有改变。服务端并不清楚咱们是否真正改变了文件,它仍然经过最后编辑时间进行判断。所以这个资源在再次被请求时,会被当作新资源,进而引起一次完整的响应——不应从新请求的时候,也会从新请求
  • 当咱们修改文件的速度过快时(好比花了 100ms 完成了改动),因为 If-Modified-Since 只能检查到以秒为最小计量单位的时间差,因此它是感知不到这个改动的——该从新请求的时候,反而没有从新请求了

为了解决这样的问题,Etag 做为 Last-Modified 的补充出现了

Etag

Etag 是由服务器为每一个资源生成的惟一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不一样,它们对应的 Etag 就是不一样的,反之亦然。所以 Etag 可以精准地感知文件的变化。

首次请求时,响应头里会携带ETag字段,像这样

ETag: W/"2a3b-1602480f459"

再次请求相同的资源,请求头里会带上If-Modified-Since字段,

值是上一次请求响应头的ETag的值

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

服务器会拿If-None-Match的值和服务器资源的当前标识对比,相同返回304, 不一样返回新的资源并带上新的Etag

Etag 的生成过程须要服务器额外付出开销,会影响服务端的性能,这是它的弊端。所以启用 Etag 须要咱们审时度势。正如咱们刚刚所提到的——Etag 并不能替代 Last-Modified,它只能做为 Last-Modified 的补充和强化存在。

Push Cache

Push Cache 是指 HTTP2 在 server push 阶段存在的缓存。

  • Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、 Service Worker Cache和 HTTP Cache 均未命中的状况下才会去询问 Push Cache。
  • Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。
  • 不一样的页面只要共享了同一个 HTTP2 链接,那么它们就能够共享同一个 Push Cache。

本地存储

Cookie 的本职工做并不是本地存储,而是“维持状态”。

在 Web 开发的早期,人们亟需解决的一个问题就是状态管理的问题:HTTP 协议是一个无状态协议,服务器接收客户端的请求,返回一个响应,故事到此就结束了,服务器并无记录下关于客户端的任何信息。那么下次请求的时候,如何让服务器知道“我是我”呢?

在这样的背景下,Cookie 应运而生。

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

Cookie 以键值对形式存储

服务器经过响应头的set-cookie: 字段来设置客户端cookie

客户端会在请求头的Cookie字段里携带cookie

客户端能够经过document.cookie来获取/设置cookie

Cookie 是有体积上限的,它最大只能有 4KB。

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

为了弥补 Cookie 的局限性,让“专业的人作专业的事情”,Web Storage 出现了。

Web Storage

Web Storage 是 HTML5 专门为浏览器存储而提供的数据存储机制。它又分为 Local Storage 与 Session Storage。

Local Storage 与 Session Storage 的区别

二者的区别在于生命周期做用域的不一样。

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

Web Storage 的特性

  • 存储容量大: Web Storage 根据浏览器的不一样,存储容量能够达到 5-10M 之间。
  • 仅位于浏览器端,不与服务端发生通讯。
  • Local Storage 与 Session Storage 在 API 方面无异, 它的原型对象上都有5个静态方法: getItemsetItemremoveItemclearkey

使用场景

  • Local Storage 用于存储一些内容稳定的资源。好比图片内容丰富的电商网站会用它来存储 Base64 格式的图片字符串, 有的网站还会用它存储一些不常常更新的 CSS、JS 等静态资源。
  • Session Storage 更适合用来存储生命周期和它同步的会话级别的信息。这些信息只适用于当前会话,当你开启新的会话时,它也须要相应的更新或释放。好比微博的 Session Storage 就主要是存储你本次会话的浏览足迹
  • Web Storage 是对 Cookie 的拓展,它只能用于存储少许的简单数据。当遇到大规模的、结构复杂的数据时,Web Storage 也心有余而力不足, 这时候就须要 IndexedDB出场了

IndexedDB

IndexedDB 是一个运行在浏览器上的非关系型数据库

理论上来讲,IndexedDB 是没有存储上限的(通常来讲不会小于 250M)。它不只能够存储字符串,还能够存储二进制数据。

下面来熟悉一下它的使用流程

  1. 建立/打开一个数据库

    if (!window.indexedDB) {
        window.alert("Your browser doesn't support a stable version of IndexedDB. Such and such feature will not be available.")
    }
    // 后面的回调中,咱们能够经过event.target.result拿到数据库实例
      let db
      // open 第一个参数为数据库名,第二个参数为版本号
      // 没有就建立,有就打开
      const request = window.indexedDB.open("xiaoceDB", 1)
      // 使用IndexedDB失败时的监听函数
      request.onerror = function(event) {
         console.log('没法使用IndexedDB')
       }
      // 成功
      request.onsuccess  = function(event){
        // 此处就能够获取到db实例
        db = event.target.result
        console.log("你打开了IndexedDB")
      }
  2. 建表(表对应indexDB数据库的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")
    	transaction.oncomplete = function(event) {
     	 	// 在全部数据添加完毕后的处理
    	};
    	transaction.onerror = function(event) {
      	// 不要忘记错误处理!
    	};
      // 拿到Object Store对象
      const objectStore = transaction.objectStore("test")
      // 向表格写入数据
      const request = objectStore.add({id: 1, name: 'xiuyan'})
      request.onsuccess = function(event) {
        // 插入成功
      };

    使用场景

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

CDN的缓存与回源机制解析

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

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

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

“缓存”就是说咱们把资源 copy 一份到 CDN 服务器上这个过程

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

CDN工做原理

CDN 每每被用来存放静态资源。上文中咱们所提到的“根服务器”本质上是业务服务器,它的核心任务在于生成动态页面或返回非纯静态页面,这两种过程都是须要计算的。业务服务器仿佛一个车间,车间里运转的机器轰鸣着为咱们产出所需的资源;相比之下,CDN 服务器则像一个仓库,它只充当资源的“栖息地”和“搬运工”。

静态资源: 就是像 JS、CSS、图片等不须要业务服务器进行计算即得的资源

动态资源: 顾名思义是须要后端实时动态生成的资源,较为常见的就是 JSP、ASP 或者依赖服务端渲染获得的 HTML 页面。

非纯静态资源: 它是指须要服务器在页面以外做额外计算的 HTML 页面。具体来讲,当我打开某一网站以前,该网站须要经过权限认证等一系列手段确认个人身份、进而决定是否要把 HTML 页面呈现给我。这种状况下 HTML 确实是静态的,但它和业务服务器的操做耦合,咱们把它丢到CDN 上显然是不合适的。

CDN 的实际应用

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

渲染篇

服务端渲染

服务端渲染的模式下,当用户第一次请求页面时,由服务器把须要的组件或页面渲染成 HTML 字符串,而后把它返回给客户端。客户端拿到手的,是能够直接渲染而后呈现给用户的 HTML 内容,不须要为了生成 DOM 内容本身再去跑一遍 JS 代码。

使用服务端渲染的网站,能够说是“所见即所得”,页面上呈现的内容,咱们在 html 源文件里也能找到

Tip: 客户端渲染: 页面上呈现的内容,你在 html 源文件里里找不到

解决的问题

  • 客户端根据j s动态加载的页面没法被搜索引擎检索到,用户搜索关键字搜不到网页,用的人就少,因此不得不启用服务端渲染
  • 用户点击咱们的连接,客户端渲染要下载html, js ,还要跑一遍j s 用户才能看到首页;而服务端渲染只须要下载html用户就能看到首页,解决了首屏加载慢的问题

应用场景

服务端渲染本质上是本该浏览器作的事情,分担给服务器去作。这样当资源抵达浏览器时,它呈现的速度就快了

可是这样会增长服务器的压力

在实践中,服务器稀少而宝贵,但首屏渲染体验和 SEO 的优化方案却不少——咱们最好先把能用的低成本“大招”都用完。

浏览器的运行机制

浏览器内核

渲染引擎和JS引擎

随着 JS 引擎愈来愈独立,内核也成了渲染引擎的代称

渲染引擎又包括了 HTML 解释器、CSS 解释器、布局、网络、存储、图形、音视频、图片解码器等等零部件

常见浏览器内核

Trident(IE) Gecko(火狐) Blink(Chrome、Opera) Webkit(Safari)

Blink 是 webkit 衍生而来的一个分支

以 webkit 内核为例,剖析浏览器的渲染过程

  1. 解析 HTML, 浏览器执行了全部的加载解析逻辑,在解析 HTML 的过程当中发出了页面渲染所需的各类外部资源请求
  2. 计算样式 ,浏览器将识别并加载全部的 CSS 样式信息与 DOM 树合并,最终生成页面 render 树
  3. 计算图层布局
  4. 绘制图层, 浏览器会根据咱们的 DOM 代码结果,把每个页面图层转换为像素,并对全部的媒体文件进行解码
  5. 整合图层,获得页面,浏览器会合并合各个图层,将数据由 CPU 输出给 GPU 最终绘制在屏幕上

初次渲染完成后,每当一个新元素加入到Dom树中,浏览器会经过c s s引擎查c s s样式表, 找到符合该元素的样式规则应用到这个元素上,而后再从新去绘制它.

在找元素样式这个过程咱们就能够进行代码层次的优化:

首先c s s引擎查找样式表的规则是,对每条样式从右到左的顺序去匹配

而并非按咱们的写法从左到右

因此这种写法耗时更长

#my li {}

c s s引擎先匹配全部li, 再去匹配li的父级是#my,

更坑的是使用通配符选择器重置样式

* {}

c s s引擎会匹配全部元素,耗费的时间更长。

  • 推荐使用语义化的类名代替父子选择器
.my_li {}
  • 尽可能不使用通配符

拒绝嵌套 拒绝嵌套 拒绝嵌套,重要的话说三遍

告别阻塞

HTML、CSS 和 JS,都具备阻塞渲染的特性。

HTML 阻塞,天经地义——没有 HTML,何来 DOM?没有 DOM,渲染和优化,都是空谈。

CSS 阻塞

前面已经说了,须要CSSOM和DOM一块儿构建Render Tree, 因此即使DOM解析完了,只要CSSOM没解析完,页面就不会渲染,因此

CSS 是阻塞渲染的资源。须要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。

也就有了

尽早: 把c s s 放在head标签里

尽快: 启用 CDN 实现静态资源加载速度的优化

JS 阻塞

正常模式引入j s文件 和内联j s代码都会阻塞dom树的解析,浏览器会暂时将控制权转交给JS引擎,j s执行完毕后, 浏览器会将控制权还给渲染引擎继续dom树的解析

外部引入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。

DOM优化

为何dom慢

把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁链接。——《高性能 JavaScript》

当咱们用 JS 去操做 DOM 时,本质上是 JS 引擎和渲染引擎之间进行了“跨界交流”。这个“跨界交流”的实现并不简单,它依赖了桥接接口做为“桥梁”。

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

当咱们对 DOM 的修改会引起它外观(样式)上的改变时,就会触发回流重绘

  • 回流:当咱们对 DOM 的修改引起了 DOM 几何尺寸的变化(好比修改元素的宽、高或隐藏元素等)时,浏览器须要从新计算元素的几何属性(其余元素的几何属性和位置也会所以受到影响),而后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。
  • 重绘:当咱们对 DOM 的修改致使了样式的变化、却并未影响其几何属性(好比修改了颜色或背景色)时,浏览器不需从新计算元素的几何属性、直接为该元素绘制新的样式(跳过了回流环节)。这个过程叫作重绘。

由此咱们能够看出,重绘不必定致使回流,回流必定会致使重绘

咱们在开发中,要从代码层面出发,尽量把回流和重绘的次数最小化

触发回流的操做:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight

谷歌浏览器的优化: 会把一段时间内的回流/重绘操做扔进一个flush队列里,不得已(上面的触发回流的操做)或者达到了必定的时间间隔再一次性出列

操做建议

  • 把获取dom和修改dom拆开,避免屡次修改同一个dom的值时获取屡次dom,
  • 使用document.createFragment建立代码片断来代替真实dom容器操做dom

Event Loop

浏览器中的事件循环:

前置知识, 异步队列:

macroTask: 宏任务队列,setTimeout、setInterval、 setImmediate、script(总体代码)、 I/O 操做、UI 渲染等

microTask: 微任务队列,process.nextTick、Promise、MutationObserver 等

  • js执行栈为空,macroTask里有script, microTask为空
  • macroTask的script出队列 压入 js执行栈 内, 执行script同步代码
  • 执行script同步代码的过程当中会产生macroTask和microTask
  • 宏任务script同步代码执行完毕,执行宏任务执行过程当中产生的全部微任务,在下一个宏任务执行前,GUI渲染线程开始工做,对页面进行渲染
  • js执行栈为空,开始下一个宏任务,周而复始

node中的事件循环

Node会先执行全部类型为 timers 的 MacroTask,而后执行全部的 MicroTask(NextTick例外)

进入 poll 阶段,执行几乎全部 MacroTask,而后执行全部的 MicroTask

再执行全部类型为 check 的 MacroTask,而后执行全部的 MicroTask

再执行全部类型为 close callbacks 的 MacroTask,而后执行全部的 MicroTask

至此,完成一个 Tick,回到 timers 阶段

……

如此反复,无穷无尽……

事件防抖与节流

// 节流, duration内不管触发多少次,只执行一次
// 使用一个变量做为节流的开关
const throttle = function (fn, duration = 300) {
  if (!fn || !(fn instanceof Function)) {
    throw('必须传一个函数做为参数')
  }
  let flag = true;
  return function(...args) {
    if (flag) {
      flag = false;
      console.log(1111)
      fn.apply(this, args)
    } else {
      return;
    }
    setTimeout(
      () => {
        flag = true;
      }, duration
    )
  }
}
const throttleFn = throttle(() => console.log('触发了滚动事件'));
document.addEventListener('scroll', throttleFn)

// 使用时间戳来判断节流的时间
const throttle1 = function (fn, duration = 300) {
  if (!fn || !(fn instanceof Function)) {
    throw('必须传一个函数做为参数')
  }
  let now = null;
  return function(...args) {
    const cur = new Date().getTime();
    if (cur - now > duration) {
      fn.apply(this, args)
      now = cur;
    }
  }
}
// 防抖 事件中止触发后的 duraion后执行回调
const debounce = function (fn, duration = 300) {
  if (!fn || !(fn instanceof Function)) {
    throw('必须传一个函数做为参数')
  }
  let timeout = null;
  return function(...args) {
    if (timeout) {
      clearTimeout(timeout);
    }
    timeout = setTimeout(() => {
      fn.apply(this, args)
    }, duration)
  }
}
document.addEventListener('scroll', debounce(() => console.log('触发了滚动事件')))

可编程的性能上报方案(W3C 性能API)

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

// 性能API
window.performance
// 控制台能够看到如下属性
eventCounts: EventCounts {size: 36}
memory: MemoryInfo {totalJSHeapSize: 35927203, usedJSHeapSize: 33471483, jsHeapSizeLimit: 4294705152}
navigation: PerformanceNavigation {type: 0, redirectCount: 0}
onresourcetimingbufferfull: null
timeOrigin: 1605666468542.5789
timing: PerformanceTiming
connectEnd: 1605666468548
connectStart: 1605666468548
domComplete: 1605666468969
domContentLoadedEventEnd: 1605666468778
domContentLoadedEventStart: 1605666468778
domInteractive: 1605666468716
domLoading: 1605666468666
domainLookupEnd: 1605666468548
domainLookupStart: 1605666468548
fetchStart: 1605666468548
loadEventEnd: 1605666468969
loadEventStart: 1605666468969
navigationStart: 1605666468542
redirectEnd: 0
redirectStart: 0
requestStart: 1605666468563
responseEnd: 1605666468655
responseStart: 1605666468654
secureConnectionStart: 0
unloadEventEnd: 1605666468664
unloadEventStart: 1605666468663
__proto__: PerformanceTiming
__proto__: Performance



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

// 关键性能指标
// 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

MDN Performance API 介绍