最详细前端性能优化

img

网站性能的好坏可以说直接决定了用户体验的好坏,而互联网的本质还是为人服务,因此极致的用户体验我想是每个公司都在追求的目标

打开一个 url 地址,浏览器都做了什么,只有理解了做了哪些事情,才知道如何针对优化。

浏览器能访问的资源都是通过 IP 访问的,但是为了方便我们记忆和使用网站都是使用的域名,所以当我们输入一个域名的时候就需要浏览器进行域名解析。

  1. 缓存中查找

浏览器缓存中查找(浏览器会缓存 DNS 一段时间)–>系统 host 文件缓存中查找–>路由器缓存中查找–>

  1. DNS 域名解析

本地域名服务器–>其他服务器查找(根域名服务器,一级域名服务器,二级域名服务器,三级域名服务器)。

从而解析到对应的 IP,浏览器主机根据 ip 地址与服务器建立 TCP 连接。

  1. TCP 三次握手

域名解析成功之后,客户端和服务器端就会有三次握手,试探链接,用语义化的语言解释就是。

客户端 ----> 服务器端 你好,我们可以链接吗

服务器端 ---->客户端 可以,你确定要连接是吧?

客户端---->服务器端 确定,我们链接吧

  1. 发送 http 请求,接收 http 响应

连接成功之后就可以开始传输数据了,传输数据需要将用户输入的 URL 封装成 HTTP Request 请求报文,发送到服务器,服务器收到请求后会发出应答,即响应数据。

  1. 断开 TCP 连接(4 次挥手)

    客户端—> 服务器端 好了,我们断开连接吧?

    服务端—> 客户端 好的,我在检查一哈有没有需要在发给你的信息?

    服务端—> 客户端 好了,我们可以断开连接了

    客户端—> 服务端 好的

  2. 浏览器解析

浏览器通过解析HTML,生成 DOM 树,解析CSS,生成 CSS 规则树,然后通过 DOM 树和 CSS 规则树生成渲染树。

根据渲染树布局,计算 CSS 样式,即每个节点在页面中的大小和位置等几何信息。HTML 默认是流式布局的,CSS 和 js 会打破这种布局,改变 DOM 的外观样式以及大小和位置。这时就要提到两个重要概念:repaint(重绘)和 reflow(回流)。

repaint:屏幕的一部分重画,不影响整体布局,比如某个 CSS 的背景色变了,但元素的几何尺寸和位置不变。

reflow: 意味着元件的几何尺寸变了,我们需要重新验证并计算渲染树。是渲染树的一部分或全部发生了变化。这就是 Reflow,或是 Layout。

性能优化工具 LightHouse

LightHouse 是 Google 开源的一个自动化工具,用于改进网络应用的质量。你可以将其作为一个 Chrome 扩展程序运行,或从命令行运行。 当为 Lighthouse 提供一个要审查的网址,它将针对此页面运行一连串的测试,然后生成一个有关页面性能的报告。可以参考失败的测试,看看可以采取哪些措施来改进应用。

——谷歌应用商店介绍

  • 首先我们可以到谷歌应用商店安装 LightHouse,点击添加至 Chrome 即可
  • 如果你的浏览器版本较新的话,可以直接打开 Chrome 开发者工具 (F12),然后打开 Audits 面板即可。Audits 面板就是集成了 LightHouse 功能的面板,我们可以根据自己的需要,选择对应的限制条件,运行 Run audits

image-20200731113042053

点击 run audits 就会自动帮我们生成性能优化报告。

这里推荐我们使用下一代图片格式化的技术,原因是 JPEG 2000、JPEG XR 和 WebP 等图像格式通常比 PNG 或 JPEG 提供更好的压缩,这意味着更快的下载速度和更少的数据消耗。

网络部分

DNS Prefetch 优化

原理

当我们访问https://www.baidu.com的时候,首先就需要把域名转化为对应的IP地址,DNS本身的解析是一个非常耗时的过程,打开DNS Prefetch 之后,浏览器会在空闲时间提前将这些域名转化为对应的 IP 地址,这里为了防止 DNS Prefetch 阻塞页面渲染影响用户体验,Chrome 浏览器的引擎并没有使用它的网络堆栈去进行预解析,而是单独开了 8 个完全异步的 Worker 线程专门负责 DNS Prefetch。

如何使用

  • **浏览器自动解析:**浏览器引擎在解析 HTML 页面的时候,会自动获取当前页面所有的 a 标签 herf 属性当中的域名,然后进行 DNS Prefetch。这里需要注意的是如果你的页面是 HTTPS,那么浏览器为了确保安全是不会开启自动解析的,这个时候我们就需要在页面头部添加如下的 meta 标签:

    <meta http-equiv="x-dns-prefetch-control" content="on" />
    

    如果你不希望浏览器开启自动解析,我们添加如下 meta 标签:

    <meta http-equiv="x-dns-prefetch-control" content="off" />
    
  • **手动解析:**直接添加如下 link 标签即可:

    <link rel="dns-prefetch" href="//www.baidu.com" />
    

DNS Prefetch 通过提前解析我们用到的一些常见域名,大大减少了实际访问时所花费的时间,是一个非常好的解决方案,而且如果你所在的公司有国际化的业务,合理地运用 DNS Prefetch 相信可以带来不错的效果。最后这里要说明一点,DNS Prefetch 的数量不是越多越好,大多数情况下我们设置 3-5 个常用的即可,多了反而会适得其反,毕竟 DNS Prefetch 也是会占用设备宽带。

Webpack 性能优化

构建速度优化
  • npm install 过程中的优化

现在一个普通的前端项目中就有几百个包,加上每个包都有相关的依赖,可想而知工作量有多大。优化的办法也很简单,就是增加版本描述文件。

  • 具体仓库地址的选择

推荐使用淘宝提供的 npm 仓库

npm config set registry https://registry.npm.taobao.org
  • 提升 Webpack 构建速度

Webpack 的打包流程

可以将其理解为一个函数,配置文件则是其参数,传入合理的参数后,运行函数就能得到我们想要的结果。

  1. 减少不必要的编译,我们在使用 loader 处理文件的时候,应该尽量把文件范围缩小,对于一些不需要处理的文件直接忽略。
module: {
  rules: [
    {
      //处理后缀名为js的文件
      test: /\.js$/,
      //exclude去掉不需要转译的第三方包 && 或者这里使用include去声明哪些文件需要被处理
      exclude: /(node_modules|bower_components)/,
      //babel的常用配置项
      use: {
        loader: "babel-loader",
        options: {
          presets: ["@babel/preset-env"],
          //缓存设置为开启
          cacheDirectory: true,
        },
      },
    },
  ];
}

这里我们对于不需要处理的第三方包直接使用 exclude 属性排除在外,或者需要处理的文件使用 include 属性去包含,此外上面我们在 options 配置当中增加了 cacheDirectory: true,这样对于转译结果就可以直接缓存到文件系统当中,在我们下次需要的时候直接到缓存当中读取即可。

  1. 使用模块热替换(HMR),传统的如果我们没有配置模块热替换,则需要每次刷新整个页面,效率很低。而使用模块热替换之后,我们只需要重新编译发生变化的模块,不需要编译所有模块,速度上面大大提高。具体配置方法如下:
module.exports = {
  ......
  plugins: [
    new webpack.HotModuleReplacementPlugin(), // 引入模块热替换插件
  ],
  devServer: {
    hot: true // 开启模块热替换模式
  }
}
  1. 由于现在的项目都会引用大量的第三方包,这些包基本都是不会变的,我们完全把他们打包到单独的文件当中,这就涉及到了公共代码的提取,optimization.splitChunks 插件

    optimization: {
            splitChunks: {
              	//设置那些代码用于分割
                chunks: "all",
             		// 指定最小共享模块数(与CommonsChunkPlugin的minChunks类似)
                minChunks: 1,
             		// 形成一个新代码块最小的体积
                minSize: 0,
                cacheGroups: {
                    framework: {
                        test: /lodash/,
                        name: "vendor",
                        enforce: true
                    }
                }
            }
        }
    
打包文件质量优化

如何使打包出的文件尽可能的小,这样我们在加载文件的时候才能更快。

1.压缩 JavaScript 代码,在压缩 JavaScript 代码的时候我们需要先将代码解析成AST 语法树,这个过程计算量非常大,我们常用的插件是 webpack-uglify-parallel 。通过 webpack-uglify-parallel 我们可以将每个资源的压缩过程交给单独的进程,以此来提升整体的压缩效率。这个插件并不在 Webpack 内部,需要我们单独安装。配置方法也比较简单,如下:

const os = require("os");
const UglifyJsParallelPlugin = require("webpack-uglify-parallel");

new UglifyJsParallelPlugin({
  //开启多进程
  workers: os.cpus().length,
  mangle: true,
  compressor: {
    //忽略警告
    warnings: false,
    //打开console
    drop_console: true,
    //打开debugger
    drop_debugger: true,
  },
});

2.压缩 CSS 代码,上面我们介绍了压缩 JavaScript 代码的方法, 同样 CSS 代码同样也可以被压缩。WebPack4.x 我们用 MiniCssExtractPlugin 和 OptimizeCSSAssetsPlugin,具体配置如下:

const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader, // 分离css代码
          "css-loader",
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "static/css/[name].[contenthash:8].css", //提取css存放目录
    }),
    new OptimizeCssAssetsPlugin(), // 使用OptimizeCssAssetsPlugin对CSS进行压缩
  ],
};

3.压缩图片,图片在一般项目当中都是最大的静态资源,所以图片的压缩就显得非常重要。图片压缩插件我们常用的是 imagemin-webpack-plugin。配置较为简单,如下:

const ImageminPlugin = require("imagemin-webpack-plugin").default;
module.exports = {
  plugins: [
    new ImageminPlugin({
      pngquant: {
        //指定压缩后的图片质量
        quality: "95-100",
      },
    }),
  ],
};
  1. 及时升级你的 Webpack 版本来获得更优的打包速度

图片优化

图片格式介绍
  • PNG:是一种无损压缩的位图图形格式,因此 PNG 格式的图片色彩表现力要比其他格式的图片更好,PNG 下又细分为 PNG-8、PNG-24、PNG-32。
  • JPG/JPEG:一种有损压缩的格式,在不影响人们可分辨图片质量的前提下,尽可能的压缩文件的大小,因此图片大小不会太大,图片质量也适中,换句话说,就是可以用最少的磁盘空间得到较好的图像质量。
  • GIF:一种基于 LZW 算法的连续色调的无损压缩格式,应用场景主要是一些动画的展示。
  • APNG:使用多个单张 PNG 连接起来的动画图片格式,支持全透明通道动画。相比于 GIF 动画,没有毛刺,质量更高,色彩效果也更好,而且它的体积相比 GIF 来说也小很多,但目前支持的浏览器并不完全。

  • SVG:是一种基于 XML 语法的图像格式,全称是可缩放矢量图。SVG 本身是可编程性的语言(支持直接插入 DOM 当中),可被非常多的工具读取和修改。SVG 与 JPEG 和 GIF 图像比起来,尺寸更小,且可压缩性更强,而且 SVG 图像可在任何的分辨率下被高质量地打印,SVG 可在图像质量不下降的情况下被放大,SVG 图像中的文本也是可选的,同时也是可搜索的(很适合制作地图)。

  • WebP:谷歌开发的一种旨在加快图片加载速度的图片格式,WebP 为网络图片提供了无损和有损压缩能力,同时在有损条件下支持透明通道。据官方实验显示:无损 WebP 相比 PNG 减少 26%大小;有损 WebP 在相同的 SSIM(Structural Similarity Index,结构相似性)下相比 JPG/JPEG 减少 25%~34%的大小;有损 WebP 也支持透明通道,大小通常约为对应 PNG 的 1/3。支持动图能力,并且也占用更小空间,更适应移动网络的动图播放。但是好东西兼容性往往都不好。

  • Base64:一种基于 64 个可打印字符来表示二进制数据的方法。实际上是一种“二进制到文本”的编码方法,它能够将给定的任意二进制数据转换(映射)为 ASCII 字符串的形式,以便在只支持文本的环境中也能够顺利地传输二进制数据。

  • BPG: 是一个新的图片格式。用来代替 jpeg 和 webp 的方案,缺点–浏览器对其的支持程度是非常低的,目前还没有大范围使用

    优势

    • 压缩比高。对于类似的质量,文件比 JPEG 小得多。
    • 支持与 JPEG(灰度,YCbCr 4:2:0,4:2:2,4:4:4)相同的色度格式,以减少转换过程中的损耗。支持 Alpha 通道。还支持 RGB,YCgCo 和 CMYK 颜色空间。
    • 支持无损压缩。
    • 可以包括各种元数据(例如 EXIF,ICC 配置文件,XMP)。
    • 动画支持。
    • 相近画质前提下比 webp 更小性能。
    • 据 mozilla 的研究,bpg 使用的 HEVC 编码比原生的 HEVC 性能更好,因为 BPG 的头部比 HEVC 的头部更小。
    • BPG 可以用于硬件上支持 HEVC 编解码器这种图片格式目前还没有被浏览器支持,需要 JavaScript 解码,但其优势非常明显。
优化方法:

图片懒加载

图片懒加载是现在最常用的性能优化手段之一,对于首屏用不到的图片,我们完全可以使用懒加载在用户下拉到对应位置的时候再进行加载,避免网页打开时一下子加载过多资源。

图片渐进显示

JPEG 还可以细分为 Baseline JPEG(标准型)、Progressive JPEG(渐进式)。

Baseline JPEG 格式:

img

Progressive JPEG(渐进式):使用了渐进显示

在这里插入图片描述

使用 Progressive JPEG 格式图片,直接使用 Photoshop,然后在保存为 JPEG 格式的时候,将连续这个选项勾选即可。

缓存优化

CDN 缓存

CDN 的全称是 Content Delivery Network,即内容分发网络,基本原理是避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容能够传输的更快,更加稳定。

**原理:**通过在各个地方部署相应的服务器,形成 CDN 集群,从而提高访问速度。

img

客户端在获得 IP 地址之后,访问最近的边缘节点。边缘节点是最小的,规模也是最小的,不会缓存所有东西。如果没有找到对应资源就会去它的上一层区域节点去寻找,如果依然没有则去中心节点寻找,如果中心节点也没有,最后再去原网站去访问。这一层一层向上找的过程我们称为回源

优化方案:

本地缓存

LocalStorage

优势

  • 大小方面相比,LocalStorage 突破了 4KB 大小体积限制,一般是 5MB(不同的浏览器大小有所区别),这相当于一个 5MB 大小的数据库提供给我们来使用,我们可以存储更多信息,这方面我们可以有更多的想象;
  • LocalStorage 是持久存储,它并不会随着页面的关闭而消失,除非我们主动去清理,不然它会一直在本地,不会过期;
  • 仅仅存储于本地,不会像 Cookie 那样,每次的 HTTP 请求都会携带。

劣势

  • 浏览器兼容性问题,IE 只有在 IE8 以上的版本才会支持;
  • 如果浏览器设置为隐私模式,那么我们无法读取 LocalStorage;
  • LocalStorage 受同源策略的限制,即协议、端口、主机地址有任何一个不同,则无法访问。
SessionStorage

SessionStorage 只在当前会话下才会起作用,一旦我们关闭当前的 Tab,SessionStorage 也就失效了。因此,SessionStorage 是一个有时效性的存储方案。

使用场景:由于 SessionStorage 具有时效性,常用的业务场景比如网站常见的游客登录,就可以存储在 SessionStorage 当中,还有网站的一些临时浏览记录都可以使用 SessionStorage 来进行记录。

Cookie

Cookie 的大小最大只有 4KB,而且它是纯文本的文件,我们每次发起 HTTP 请求都会携带 Cookie。

特性

  • Cookie 一旦创建成功,那么名字无法进行修改;
  • Cookie 无法跨域名,这是由 Cookie 隐私安全性所决定的,这样能够阻止非法获取其它网站的 Cookie;
  • 每个单独的域名下面的 Cookie 数量不能超过 20 个。
IndexedDB

IndexedDB 是非关系型数据库(类 NOSQL),其实它和我们当下非常流行的 MongoDB 非常相似,我们直接使用 JavaScript 语言就可以直接进行相关的操作,不需要别的语言,大大降低了学习成本。

合理利用缓存是前端工程师的必修课

HTTP 缓存策略

在概念层面 HTTP 缓存策略又细分为强制缓存和协商缓存

最完美的效果就是我们发起请求然后取到相应的静态资源。如果服务端的静态资源没有更新,我们就下次访问的时候直接从本地读取即可;如果服务端的静态资源已经更新,那么我们再次请求的时候就要到服务器请求最新的资源,然后进行拉取。

强制缓存

强缓存直接从缓存数据库中取出资源,无需再发送请求到服务器上

  • **expires **

expires 是 HTTP1.0 的字段,通过指定一个具体的绝对时间值作为缓存资源的过期时间

img

  • Cache-Control

Cache-Control 是 HTTP1.1 才有的字段,Cache-Control 设置的是一个相对时间,单位是秒,可以更加精准地控制资源缓存

img

Cache-Control 可设置的字段值较多,下面我们一一来介绍。

  • public:设置了该字段值的资源表示可以被任何对象(包括:发送请求的客户端、代理服务器等等)缓存。这个字段值不常用,一般还是使用**max-age=**来精确控制;
  • private:设置了该字段值的资源只能被用户浏览器缓存,不允许任何代理服务器缓存。在实际开发当中,对于一些含有用户信息的 HTML,通常都要设置这个字段值,避免代理服务器(CDN)缓存;
  • no-cache:设置了该字段需要先和服务端确认返回的资源是否发生了变化,如果资源未发生变化,则直接使用缓存好的资源;
  • no-store:设置了该字段表示禁止任何缓存,每次都会向服务端发起新的请求,拉取最新的资源;
  • max-age=:设置缓存的最大有效期,单位为秒;
  • s-maxage=:优先级高于 max-age=,仅适用于共享缓存(CDN),优先级高于max-age或者Expires头;
  • max-stale[=]:设置了该字段表明客户端愿意接收已经过期的资源,但是不能超过给定的时间限制。

上面就是强制缓存的常用字段,实际开发当中 expires 和 Cache-Control 一般都要进行设置,这是为了兼容不支持 HTTP1.1 的环境。两者同时存在,Cache-Control 的优先级要高于 expires。

强缓存状态码为200

  • from memory cache(缓存资源在内存中)
  • from disk cache(缓存资源在硬盘中)

协商缓存

对比缓存是需要经过服务器确认是否使用缓存的机制,其 http 状态码为304,意为not modified

如果没有命中强制缓存,如果设置了协商缓存,这个时候协商缓存就会发挥作用了。

  • Last-Modified/If-Modified-Since

最后一次的修改时间,设置方法和我们上面讲的强制缓存的设置方法一样,都是设置一个时间戳,同样它也是由服务端放到 Response Headers 返回给我们

image-20200802183724257

如果有设置协商缓存,我们在首次请求的时候,返回的 Response Headers 会带有 Last-Modified。当再次请求没有命中强制缓存的时候,这个时候我们的 Request Headers 就会携带 If-Modified-Since 字段,它的值就是我们第一次请求返回给我们的 Last-Modified 值。服务端接收到资源请求之后,根据 If-Modified-Since 的字段值和服务端资源最后的修改时间是否一致来判断资源是否有修改。如果没有修改,则返回的状态码为 304;如果有修改,则返回新的资源,状态码为 200

缺陷

  1. 服务端对 Last-Modified 标注的最后修改时间只能精确到秒级,如果某些文件在 1 秒钟以内被修改多次的话,这个时候服务端无法准确标注文件的修改时间。
  2. 服务端有时候会定期生成一些文件,有时候文件的内容并没有任何变化,但这个时候 Last-Modified 会发生改变,导致文件无法使用缓存。
  • Etag/If-None-Match

通常是根据文件的具体内容计算出一个 hash 值,只要文件的内容不变,它就不会发生改变,保证了唯一性,这一点可以类比人的指纹。

image-20200802184000092

如果我们有设置协商缓存,在首次请求的时候,返回的 Response Headers 会带有 Etag 值。当再次请求没有命中强制缓存的时候,这个时候我们的 Request Headers 就会携带 If-None-Match 字段,它的值就是我们第一次请求返回给我们的 Etag 值。服务端再用 Etag 来进行比较,如果相同就直接使用缓存,如果不同再从服务端拉取新的资源。

nginx 缓存配置
  • expires 和 cache-control

打开 Nginx 的配置文件 nginx.conf

// expires:给图片设置过期时间30天,这里也可以设置其它类型文件
location ~ \.(gif|jpg|jpeg|png)$ {
        root /var/www/img/;
        expires 30d;
}
// cache-control:给图片设置过期时间36秒,这里也可以设置其它类型文件
location ~ \.(gif|jpg|jpeg|png)$ {
        root /var/www/img/;
        add_header    Cache-Control  max-age=3600;
}
  • Last-Modified 和 Etag

Last-Modified 在 Nginx 当中是默认启用的。

Etag 需要单独配置

需要先安装 Etag 模块;安装成功之后,仍然打开 nginx.conf 文件,确保当中没有出现 etgoff,然后添加如下配置,即可开启 Etag:

location ~ .*\.(gif|jpg|jpeg|png)$ {
   FileETag on;
   etag_format "%X%X%X"; //这里格式化规则可以修改
 }
webpack 打包缓存配置
module.exports = {
    entry: {
        index: './test/test.js',
        about: './test/about.js'
    },
    output: {
    		// 打包出来的JavaScript文件用chunkhash
        filename: '[name].[chunkhash:8].js',
        path: path.resolve(__dirname, 'dist')
    },
  	module: {
        rules: [
            {
              	test: /\.css$/,
               	use: [{loader: MiniCssExtractPlugin.loader},'css-loader']
            },
            {
              	 test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
       					 loader: 'file-loader',
                 query: {
                     // 图片文件使用hash
                     name: '[name].[ext]?[hash]',
                     outputPath: 'static/img/',
                     publicPath: '/dist/static/img/'
                 }
            }
        ]
    },
   plugins: [
        new CleanWebpackPlugin(),
     		new MiniCssExtractPlugin({
     		    // 抽离的CSS文件用contenthash
            filename: '[name].[contenthash:8].css',
	          chunkFilename: '[id].css'
        }),
    ]
}

渲染与性能优化

浏览器的渲染引擎

  • IE(Trident)
  • Chrome(Blink)
  • Firefox(Gecko)
  • Opera(Blink)
  • Safari(Webkit)
  • UC(U3)
  • QQ 浏览器微信(X5/Blink)

img

解析 HTML 和解析 CSS 是并行处理的,如果遇到 <script>标签会停止解析,先执行标签当中 JavaScript;如果是外联方式,也需要等待下载并且执行完对应的 JavaScript 代码,然后才能够继续执行解析 HTML 的工作。

针对渲染原理进行相关优化

css 优化

CSS 解析和 HTML 解析是同步进行的,那么一个 HTML 文档首先解析的肯定是 HTML,然后才是 CSS,这就导致了 HTML 解析完成后,往往需要等待 CSS 解析。如果 CSS 没有解析完成,我们就需要一直等,这里就是 CSS 阻塞了相关的渲染。因此针对这种情况,我们往往把 CSS 样式表全部通过 <style> 标签内联到网页当中

js 优化

JavaScript 既会阻塞 HTML 解析,也会阻塞 CSS 解析。因此我们可以改变 JavaScript 的加载方式或者加载时机来进行优化:

  1. 尽量将 JavaScript 文件放在 body 的底部;
  2. body 中间尽量不要写 <script> 标签;
  3. 我们都知道通过 <script> 标签引入 JavaScript 代码的方式共有 3 种,一个就是直接引入,另外两种分别用添加 async 属性和 defer 属性的方式引入。如果设置为 defer,那么整个 JavaScript 的加载是异步的,并且在 DOMContentLoaded 事件之后才会执行当中的代码;如果设置为 async,整个 JavaScript 的加载是异步的,但是不会阻塞浏览器的任何操作,加载完成后执行相关代码。所以这里的最佳实践是 async 执行时机不确定,不建议用于业务代码,但可用于单独的代码,如第三方统计代码。而 defer 是在 DOMContentLoaded 事件之后执行,所以 defer 一般用于业务代码。

首屏空白优化

随着 React、Vue 等框架的流行,SPA 应用越来越多,由于 SPA 应用打包生成的 JavaScript 文件非常大,首屏都是等这个巨大的 JavaScript 文件加载完成之后,才能够开始渲染,这就导致首屏加载必然会出现白屏问题。

骨架屏

骨架屏就是我们页面在未完成加载的时候,先用一些简单图形大概勾勒出整个页面,给用户视觉上更好的体验,而不是整个页面都是白屏。等到资源加载完毕,再把骨架屏替换掉即可。整个过程我们用一张图来表示就非常容易理解

img

目前使用比较广泛的是 page-skeleton-webpack-plugin 这个插件,它是一个 Webpack 插件,可以根据具体的页面生成对应的骨架屏。具体的配置方案这里写得非常详细,这里不再详细罗列,而且它支持动态调试,对于多个页面的使用非常方便。

使用 Code Splitting

代码分割(Code Splitting)是指将项目代码构建打包(Bundling)后,根据指定规则分割成多个 bundles(输出文件),这些模块文件可以被按需动态加载或者并行加载,可以用来优化代码加载时的资源大小及优先级。正确使用代码分割,可以优化提升资源加载效率。

使用按需加载。按需加载的方案非常多,最常用的就是使用 ES6 新增的 import() 方法。使用方法也非常简单,我们只需要在我们的路由文件当中引入即可,如下:

const Test1 = () =>
  import(
    /* webpackChunkName: "Test1" */
    "./pages/Test1.vue"
  );

const Test2 = () =>
  import(
    /* webpackChunkName: "Test2" */
    "./pages/Test2.vue"
  );

这里 Webpack 配置的输出也要做对应的修改,如下:

module.exports = {
  output: {
    chunkFilename: "[name].chunk.js",
  },
};
使用资源预加载

简单来说就是在当前页面加载完成后或者其它空闲时间,我们可以加载下面页面会用到的资源。

 <link rel="preload" href="style.css" as="style">
 <link rel="preload" href="main.js" as="script">

我们预加载了 CSS 和 JavaScript 文件。所以在随后的页面渲染中,一旦需要使用它们,它们就会立即可用。

DOM 的优化

在日常开发当中,不管我们是否使用框架,书写 HTML 是不可避免的,并且操作 DOM 是非常耗费性能的,如果操作错误还会引起回流。

  1. 注意语义化:有利于浏览器解析时间,而且对搜索引擎的抓取也是十分友好的。
  2. 尽量减少无用的标签:不要嵌套多个无用的 div,对一下没有用的 div 要及时删除,能用 CSS 实现的,尽量不要使用相关的 HTML 元素。

重绘与回流

回流:当有元素的大小、布局、结构等发生改变时,就会触发回流,每个页面在首次加载的时候都需要进行至少一次回流。

重绘:有元素属性需要更新,这些属性只是影响元素的外观、风格,那么则会触发重绘。

如何减少重绘与回流

  • 避免使用表格布局方法
  • 减少触发它们操作的次数
  • 把相关的属性的缓存起来,然后最后在对其进行处理
  • 修改样式的时候使用类名的方式一次性进行修改,而不是使用 style 的方式
  • 对一些涉及到复杂动画的元素,在不影响相关布局的情况下,尽量将其 position 属性设置为 absolute 或者 fixed

Event Loop

JavaScript 是单线程的语言,它在一个时间点只能处理一件事情,完成一件事情之后才可以继续另外一件事情。JavaScript 为了解决这个问题,于是产生了使用异步这种方式来模拟多线程,而支撑异步的就是 Event Loop。

img

  • heap

heap 是堆这种数据结构,堆其实就是我们平时所说的二叉树,这里存放的主要是 JavaScript 当中的对象,也是 Event Loop 当中一个重要的环节。

  • stack

栈,后进先出,代码执行的时候,会将代码压入栈中进行执行,当任务完成之后,根据栈后进先出的特点,再将各个任务进行出栈。

假定没有异步任务的情况下 JavaScript 的执行顺序,当我们遇到异步任务,比如:setTimeout、addEventListener 这类异步任务的时候,这个时候异步任务不会直接被压入到栈中,而是会交给浏览器的 Web API 进行维护,当这些异步任务执行完成之后,会在任务队列当中放置对应事件。当执行栈当中任务为空的时候,然后浏览器读取任务队列,再把对应的异步任务压入到执行栈执行,这就是我们经常说的事件循环。

  • Macrotask/Microtask

任务队列 Microtask 优先级是高于 Macrotask 的,Microtask 当中任务不会一个一个压入执行栈,而是所有任务直接压入栈中,当 Microtask 当中的任务执行完毕后,然后我们再从 Macrotask 中取栈顶的第一个任务进行执行。

那么哪些任务属于 Microtask,哪些任务属于 Macrotask 呢,这里我做了一个大概的总结,如下:

Macrotask:setTimeoutsetIntervalI/OUI Renderingscript当中的所有代码setImmediate(Node)

Microtask:process.nextTick(node)PromiseMutationObserver

优化方法

当开始执行的时候,首先 script 标签当中的代码出 Macrotask,被压入到执行栈执行,这个时候就会有对应的任务被推入 Macrotask 和 Microtask 当中,上面我们已经说过 Microtask 是所有任务一起执行,而 Macrotask 则是任务一个一个执行,那么页面渲染是在 Microtask 之后才进行的,如果我们在异步操作当中进行 DOM 操作,我们尽量将这个操作用 Microtask 当中的任务包一下,这样我们就可以在页面渲染之前就执行这个 DOM 操作了。

防抖节流

点击这里可以体验防抖和节流的效果

节流(throttle)

不管你在一段时间内如何不停地触发事件,只要设置了节流,就会每隔一段时间执行一次

//防抖
const debounce = (() => {
  let timeout;
  return (callback, wait) => {
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(callback, wait);
  };
})();
防抖(debounce)

防抖的原理则是不管你在一段时间内如何不停的触发事件,只要设置了防抖,则只在触发 n 秒后才执行。

// 防抖
const debounce = (() => {
  let timer = 0;
  return (callback, wait) => {
    clearTimeout(timer);
    timer = setTimeout(callback, wait);
  };
})();

服务端渲染

服务端渲染在一般场景下其实是无需使用的,虽然它可以解决白屏问题以及 SEO 优化,但是它对服务端要求非常高

  • 客户端渲染(CSR)

页面初始加载的 HTML 文档中无内容,需要下载执行 JS 文件,由浏览器动态生成页面,并通过 JavaScript 进行页面交互事件与状态管理,这样的页面渲染采用的就是客户端渲染。

  • 服务端渲染(SSR)

在服务器端执行一次,用于实现服务器端渲染(首屏直出);在客户端再执行一次,用于接管页面交互。这样页面渲染采用的就是服务端渲染。

  • 本地离线渲染(NSR)

通过离线资源和预加载的方式,native 提前完成渲染页面并放入缓存,等用户点击的时候瞬间呈现页面

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值