前端性能优化

2020/12/27 性能优化

🌙 前端性能优化

🌙 1.浏览器

🌙 1.1如何使用performance工具查看程序性能 (opens new window)

  • FP: First Paint
  • FCP: First Contentful Paint
  • FMP: First Meaningful Paint
  • TTI: Time To Interactive

First Contentful Paint (FCP) and First Meaningful Paint (FMP) Explained (opens new window)

火焰图表线条含义:

  • 蓝线:代表DOMContentLoaded事件。
  • 绿线:表示第一次绘制的时间。
  • 红线:代表load事件。

彩色条含义:

  • HTML文件是 blue (蓝色) 的。
  • JS是 yellow (黄色) 的。
  • CSS表是 purple (紫色) 的。
  • 媒体文件是 green (绿色) 的。
  • 其他是 grey (灰色) 的。


🌙 1.2使用Show Coverage查看代码覆盖率

chrome浏览器,调试console界面,ctrl+shift+p,输入show Courage

使用异步加载优化, 如:

function handleClick(){
    const dom = document.createElement('div');
    dom.innerHTML = 'Hello Webpack';
    document.body.appendChild(dom);
}

export default handleClick;
1
2
3
4
5
6
7
document.addEventListener('click', ()=>{
    // 异步加载,并在空闲的时候预加载
    import(/* webpackPrefetch: true */ './handle').then(({ default:click })=>{
        click();
    })
})
1
2
3
4
5
6

推荐: 【webpack】快速笔记18 -- 擅用show Coverage以及 preloading与prefetching (opens new window)

🌙 1.3 使用lighthouse查看页面整体性能

Lighthouse的使用与Google的移动端最佳实践 (opens new window)

How to measure speed? (opens new window)

工具:

🌙 1.4 减少浏览器的重绘和回流 (opens new window)

reflow-chart

重绘

由于节点的几何属性发生改变或者由于样式发生改变而不会影响布局的,称为重绘,例如outline, visibility, color、background-color等,重绘的代价是高昂的,因为浏览器必须验证DOM树上其他节点元素的可见性。

回流

回流必定会发生重绘,重绘不一定会引发回流。 回流是布局或者几何属性需要改变就称为回流。回流是影响浏览器性能的关键因素,因为其变化涉及到部分页面(或是整个页面)的布局更新。一个元素的回流可能会导致了其所有子元素以及DOM中紧随其后的节点、祖先节点元素的随后的回流。

哪些会导致回流?

+ 添加或删除可见的DOM元素
+ 元素的位置发生变化
+ 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
+ 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
+ 页面一开始渲染的时候(这肯定避免不了)
+ 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

注意:回流一定会触发重绘,而重绘不一定会回流

怎么减少回流?

针对CSS

- 使用transform代替top

- 使用 visibility 替换 display: none

  为前者只会引起重绘,后者会引发回流(改变了布局)

- 避免使用table布局

  可能很小的一个小改动会造成整个 table 的重新布局

- 尽可能在DOM树的最末端改变class

  回流是不可避免的,但可以减少其影响。尽可能在DOM树的最末端改变class,可以限制了回流的范围,使其影响尽可能少的节点

- 避免设置多层内联样式

  CSS 选择符从右往左匹配查找,避免节点层级过多

- 将动画效果应用到position属性为absolute或fixed的元素上

  避免影响其他元素的布局,这样只是一个重绘,而不是回流,同时,控制动画速度可以选择 requestAnimationFrame,详见探讨 requestAnimationFrame。

- 避免使用CSS表达式,可能会引发回流

- 将频繁重绘或者回流的节点设置为图层

  图层能够阻止该节点的渲染行为影响别的节点,例如will-change、video、iframe等标签,浏览器会自动将该节点变为图层。

- CSS3 硬件加速(GPU加速)

  使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

  - transform、opacity、filters、will-changeition-absolute 或 position-fixed 来实现此目的。

针对JS

- 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
- 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
- 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
- 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

- 使用cssText一次性改变样式

- 修改class类名来操作样式

- 批量修改DOM先让DOM脱离文档流

  1.使元素脱离文档流
  2.对其进行多次修改
  3.将元素带回到文档中。

有三种方式可以让DOM脱离文档流

1.隐藏元素,应用修改,重新显示

2.使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档。

3.将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。

介绍下重绘和回流(Repaint & Reflow),以及如何进行优化 (opens new window)

🌙 2 网络优化

从输入url到页面渲染 (opens new window)的时间怎么变短?

🌙 2.1 DNS查询:

  • 浏览器缓存
  • 本机缓存
  • hosts文件
  • 路由器缓存
  • ISP DNS缓存
  • DNS递归查询(可能存在负载均衡导致每次IP不一样)

🌙 2.2 DNS预解析

浏览器会在加载网页时对网页中的域名进行解析缓存,这样在你单击当前网页中的连接时就无需进行 DNS 的解析,减少用户等待时间,提高用户体验。

<!--<meta>信息告诉浏览器,当前页面要做DNS预解析;content="off"禁用-->
<meta http-equiv="x-dns-prefetch-control" content="on" />
<!--使用<link>标签来强制对DNS预解析-->
<link rel="dns-prsfetch" href="//g.alicdn.com"></link>
1
2
3
4

DNS预解析dns-prefetch是什么及怎么使用 (opens new window)

Web 性能优化:prefetch, prerender (opens new window)

使用 Preload/Prefetch 优化你的应用 (opens new window)

🌙 2.3 TCP分段和IP分片

TCP报文段如果很长的话,会在发送时发生分段,在接受时进行重组,同样IP数据包在长度超过一定值时也会发生分片,在接收端再将分片重组。

TCP/IP详解--TCP的分段和IP的分片 (opens new window)

🌙 2.4 TCP慢启动 (opens new window)

根据网络情况逐步增加每次发送的数据量,让网络包的大小逐渐匹配网速,从而防止网络的拥塞现象。

TCP的智慧—慢启动,拥塞避免,快速重传和快速恢复 (opens new window)

🌙 2.5 减少HTTP请求

一个完整的 HTTP 请求需要经历 DNS 查找,TCP 握手,浏览器发出 HTTP 请求,服务器接收请求,服务器处理请求并发回响应,浏览器接收响应等过程。

🌙 2.6 使用 HTTP2

解析速度快、多路复用(多个请求可以共用一个 TCP 连接)、头部压缩、服务器推送

🌙 2.7 压缩 HTTP 的请求和响应 (opens new window)

🌙 2.8 使用服务端渲染(ssr):

🌙 2.9 静态资源使用 CDN(内容分发网络)

在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。

基于TCP之上的协议,如HTTP,传输过程要在加上其头部信息并且还需要解析之后TCP才能识别,这部分信息也会影响网络传输速度。如果只是单纯的发送一些数据,可以不走HTTP,可以直接基于TCP之上定制自己的client和server,以减少包的大小,从而达到加快传输的目的。

问题再现:

🌙 3 图片优化

web 图像技术:前端引入图片的各种方式及其优缺点 (opens new window)

🌙 3.1 使用字体图标 iconfont 代替图片图标

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

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

fontmin-webpack (opens new window)

Iconfont-阿里巴巴矢量图标库 (opens new window)

🌙 3.2 图片懒加载:

在页面中,先不给图片设置路径,只有当图片出现在浏览器的可视区域时,才去加载真正的图片,这就是图片懒加载。

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

<img data-src="https://assets/xx.jpg>
1

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

<script>
const img = document.querySelector('img') 
img.src = img.dataset.src
</script>
1
2
3
4

还可以使用HTML5中的loading属性,处于实验阶段 (opens new window)

loading:指示浏览器应当如何加载该图像。

loading:eager立即加载图像,不管它是否在可视视口(visible viewport)之外(默认值)。

loading: lazy延迟加载图像,直到它和视口接近到一个计算得到的距离,由浏览器定义。

web 前端图片懒加载实现原理 (opens new window)

🌙 3.3 响应式图片

浏览器能够根据屏幕大小自动加载合适的图片:

  • 通过 picture 实现:
<picture>
	<source srcset="banner_w1000.jpg" media="(min-width: 801px)">
	<source srcset="banner_w800.jpg" media="(max-width: 800px)">
	<img src="banner_w800.jpg" alt="">
</picture>
1
2
3
4
5
  • 通过 @media 实现:
@media (min-width: 769px) {
	.bg {
		background-image: url(bg1080.jpg);
	}
}
@media (max-width: 768px) {
	.bg {
		background-image: url(bg768.jpg);
	}
}

1
2
3
4
5
6
7
8
9
10
11

🌙 3.4 使用缩略图

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

所以,我们可以用两张图片来实行优化。

一开始,只加载缩略图,当用户悬停在图片上时,才加载大图。

还有一种办法,即对大图进行延迟加载,在所有元素都加载完成后手动更改大图的 src 进行下载。

🌙 3.5 合理选择图片格式

  • 下一代图片格式 AVIF(AV1 Image File Format)已经来了,此格式比 JPEG 小 50% 左右、比 WebP 小 20% 左右,相关性能跑分见此,目前 Chrome 85、Firefox 77 已支持;

前端该如何选择图片的格式 (opens new window)

每个前端工程师都应该了解的图片知识 (opens new window)

WebP 相对于 PNG、JPG 有什么优势? (opens new window)

🌙 3.6 压缩图片

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

npm i -D image-webpack-loader
1

webpack 配置:

{
  test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
  use:[
    {
    loader: 'url-loader',
    options: {
      limit: 10000, /* 图片大小小于1000字节限制时会自动转成 base64 码引用*/
      name: utils.assetsPath('img/[name].[hash:7].[ext]')
      }
    },
    /*对图片进行压缩*/
    {
      loader: 'image-webpack-loader',
      options: {
        bypassOnDebug: true,
      }
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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

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

🌙 3.8 使用 CSS Sprites 雪碧图

webpack插件: webpack-spritesmith (opens new window)

浅谈 CSS Sprites 雪碧图应用 (opens new window)

🌙 3.9 处理加载失败的图片

  • 1.图像注明alt属性,将图像加载error的时候,新增一个错误类名,例如.error
<img src="zxx.png" alt="CSS新世界封面" onerror="this.classList.add('error');">
1
  • 2.配合使用如下所示的CSS,并将alt属性显示出来:
img.error {
  display: inline-block;
  transform: scale(1);
  content: '';
  color: transparent;
    &::before {
       content: '';
       position: absolute;
       left: 0; 
       top: 0;
       width: 100%; 
       height: 100%;
       background: #f5f5f5 url('break.svg') no-repeat center / 50% 50%;
     }
    &::after {
       // 展示alt属性,标明加载失败的图像的内容
       content: attr(alt);
       position: absolute;
       left: 0; bottom: 0;
       width: 100%;
       line-height: 2;
       background-color: rgba(0,0,0,.5);
       color: white;
       font-size: 12px;
       text-align: center;
       white-space: nowrap;
       overflow: hidden;
       text-overflow: ellipsis;
     }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

参考:图片加载失败后CSS样式处理最佳实践 (opens new window)

🌙 3.10 图片预加载

HTML5中有原生的预加载属性,名为prefetchprerender:

<!--IE11及以上,兼容性更好-->
<link rel="prefetch" href="(url)">
<!--prefetcher:浏览器会在后台(页面不可见)的位置预加载,相比prefetch,兼容性要差一些-->
<link rel="prefetcher" href="(url)">
1
2
3
4

或者使用JS:

function preloadImg(url) {
    const img = new Image();
    img.src = url;
    if(img.complete) {
        //接下来可以使用图片了
        //do something here
    }
    else {
        img.onload = function() {
            //接下来可以使用图片了
            //do something here
        };
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

还可以提高图片的加载

张鑫旭:基于用户行为的图片等资源预加载 (opens new window)

🌙 3.11 降低/提升图像加载优先级

<img>标签的importance属性可以指示下载资源时相对重要性,或者说优先级。允许的值:

  • importance:auto:不指定优先级。浏览器可以使用自己的算法来为图像选择优先级。

  • importance:high:此图像在下载时优先级较高。

  • importance:low:此图像在下载时优先级较低。

<!-- An image the browser assigns "High" priority, but we don't actually want that. -->
<img src="/images/in_viewport_but_not_important.svg" importance="low" alt="I'm an unimportant image!">
1
2

当然不仅仅img可以,我们还可以给js加上:

<!-- We want to initiate an early fetch for a resource, but also deprioritize it -->
<link rel="preload" href="/js/script.js" as="script" importance="low">
<script src="/js/app.js" defer importance="high"></script>
1
2
3

甚至,使用fetchAPI也可以加上这个属性:

fetch("https://example.com/", {importance: "low"}).then(data => {
    // Do whatever you normally would with fetch data
});
1
2
3

Get Ready for Priority Hints (opens new window)

🌙 4.React性能优化 (opens new window)

🌙 4.1代码拆分(Code Splitting) (opens new window)

React Router实践: 使用react-loadablereact-router-config

  • 配置app.tsx, 挂载DOM:
import { Provider } from 'mobx-react';
import React from 'react';
import ReactDOM from 'react-dom';
import { renderRoutes } from 'react-router-config';
import { HashRouter } from 'react-router-dom';
import './index.scss';

import routes from './routes';
import stores from './stores';

ReactDOM.render(
  <HashRouter>
    // 此处渲染路由
    <Provider {...stores}>{renderRoutes(routes)}</Provider>
  </HashRouter>,
  document.getElementById('root')
);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • 配置routes.ts文件:
import Loadable from 'react-loadable';
import Loading from '../components/Loading';
import { RouteConfig } from 'react-router-config';

const routes: RouteConfig[] = [
  {
    path: '/',
    component: Loadable({
      // 此处为页面布局
      loader: () => import('./Layout'),
      loading: Loading,
    }),
    routes: [
      {
        path: '/page-a',
        exact: true,
        component: Loadable({
          loader: () =>
            import('./pages/page-a'),
          loading: Loading,
        }),
      },
      {
        path: '/page-b',
        exact: true,
        component: Loadable({
          loader: () =>
            import('./pages/page-b'),
          loading: Loading,
        }),
      },
    ],
  },
];

export default routes;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
  • 配置页面布局Layout.tsx:
import React from 'react';
import { renderRoutes, RouteConfigComponentProps } from 'react-router-config';

import TopBar from './components/topbar';
import Sidebar from './components/sidebar';
import Footer from './components/footer';

const Layout: React.FC<RouteConfigComponentProps<void>> = ({route}) => {
  return (
    <>
        <TopBar />
        <Sidebar />
        <div className="main">{route && renderRoutes(route.routes)}</div>
        <Footer />
    </>
  );
};

export default Layout;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

🌙 4.2优化组件

掘金:React性能优化 (opens new window)

React官方:React性能优化 (opens new window)

🌙 5.webpack打包优化

🌙 5.1打包分析webpack-bundle-analyzer

npm install webpack-bundle-analyzer -D
1
//webpack.config.prod.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.config.base');
module.exports = merge(baseWebpackConfig, {
    //....
    plugins: [
        //...
        new BundleAnalyzerPlugin(),
    ]
})
1
2
3
4
5
6
7
8
9
10
11

🌙 5.2 升级webpack

一次webpack3升级为webpack4的实践 (opens new window)

webpack5已发布 (opens new window)

🌙 5.3配置source-map

当添加source-map后,在dist文件夹之后就会生成一个.map文件

在生产环境建议使用,cheap-module-source-map 在开发环境中建议使用,cheap-module-eval-source-map

eval:的特点:

  1. 打包速度最快
  2. 针对于复杂的代码eval提示出来的错误坑可能不全
module.exports = {
    mode:"development",//代表的为开发环境
    devtool:"inline-source-map"//代表的是当我打包之后,在dist文件下不会生成一个.map的文件,但是会在index.js中生成一个base64的字符串
}
1
2
3
4

🌙 5.4 指定打包文件exclude/include

我们可以通过 exclude、include 配置来确保转译尽可能少的文件。顾名思义,exclude 指定要排除的文件,include 指定要包含的文件。 exclude 的优先级高于 include,在 include 和 exclude 中使用绝对路径数组,尽量避免 exclude,更倾向于使用 include。

//webpack.config.js
const path = require('path');
module.exports = {
    //...
    module: {
        rules: [
            {
                test: /\.js[x]?$/,
                use: ['babel-loader'],
                include: [path.resolve(__dirname, 'src')]
            }
        ]
    },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

🌙 5.5使用缓存cache-loader

在一些性能开销较大的 loader 之前添加 cache-loader,将结果缓存中磁盘中。默认保存在 node_modueles/.cache/cache-loader 目录下。 首先安装依赖:

npm install cache-loader -D
1

复制代码 cache-loader 的配置很简单,放在其他 loader 之前即可。修改Webpack 的配置如下:

module.exports = {
    //...
    

    module: {
        //我的项目中,babel-loader耗时比较长,所以我给它配置了`cache-loader`
        rules: [
            {
                test: /\.jsx?$/,
                use: ['cache-loader','babel-loader']
            }
        ]
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

如果只打算给 babel-loader 配置 cache 的话,也可以不使用 cache-loader,给 babel-loader 增加选项 cacheDirectory

🌙 5.6多线程构建thread-loader

thread-loader 会将您的 loader 放置在一个 worker 池里面运行,以达到多线程构建。

🌙 5.7开启多进程压缩terser-webpack-plugin (opens new window)

const TerserPlugin = require('terser-webpack-plugin'); 
module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
      }),
    ],
  },
};
Num
1
2
3
4
5
6
7
8
9
10
11
12

🌙 5.8缓存第一次构建hard-source-webpack-plugin

配置 hard-source-webpack-plugin,首次构建时间没有太大变化,但是第二次开始,构建时间大约可以节约 80%。

🌙 5.9noParse标记不进行转化和解析

如果一些第三方模块没有AMD/CommonJS规范版本,可以使用 noParse 来标识这个模块,这样 Webpack 会引入这些模块,但是不进行转化和解析,从而提升 Webpack 的构建性能 ,例如:jquery 、lodash。

🌙 5.10配置resolve加快查找模块

resolve 配置 webpack 如何寻找模块所对应的文件

🌙 5.11 IgnorePlugin (opens new window)忽略第三方包指定目录

//webpack.config.js
module.exports = {
  //...
  plugins: [
    //忽略 moment 下的 ./locale 目录
    new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
  ]
}
1
2
3
4
5
6
7
8

🌙 5.12externals不打包使用CDN引入的js

🌙 5.13使用DllPlugin (opens new window)拆分bundles

DllPlugin 和 DLLReferencePlugin 可以实现拆分 bundles,并且可以大大提升构建速度,DllPlugin 和 DLLReferencePlugin 都是 webpack 的内置模块。

🌙 5.14抽离公共代码optimization.splitChunks

抽离公共代码是对于多页应用来说的,如果多个页面引入了一些公共模块,那么可以把这些公共的模块抽离出来,单独打包。公共代码只需要下载一次就缓存起来了,避免了重复下载。 抽离公共代码对于单页应用和多页应该在配置上没有什么区别,都是配置在 optimization.splitChunks 中。

🌙 5.15tree-shaking (opens new window)

  • 嵌套的 tree-shaking
  • 内部模块 tree-shaking
  • CommonJs Tree Shaking

webpack4只支持ES6的tree shaking,不支持 CommonJs 导出和 require()

webpack 5 增加了对一些 CommonJs 构造的支持,允许消除未使用的 CommonJs 导出,并从 require() 调用中跟踪引用的导出名称。

理解: 你可以将应用程序想象成一棵树。绿色表示实际用到的 source code(源码) 和 library(库),是树上活的树叶。灰色表示未引用代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。

你的Tree-Shaking并没什么卵用 (opens new window)

Tree-Shaking性能优化实践 - 原理篇 (opens new window)

Tree-Shaking性能优化实践 - 实践篇 (opens new window)

🌙 5.16 scope hosting 作用域提升

变量提升,可以减少一些变量声明。在生产环境下,默认开启。

🌙 5.17babel 配置的优化

如果你对 babel 还不太熟悉的话,那么可以阅读这篇文章:不容错过的 Babel7 知识。 在不配置 @babel/plugin-transform-runtime 时,babel 会使用很小的辅助函数来实现类似 _createClass 等公共方法。默认情况下,它将被注入(inject)到需要它的每个文件中。但是这样的结果就是导致构建出来的JS体积变大。 我们也并不需要在每个 js 中注入辅助函数,因此我们可以使用 @babel/plugin-transform-runtime,@babel/plugin-transform-runtime 是一个可以重复使用 Babel 注入的帮助程序,以节省代码大小的插件。 因此我们可以在 .babelrc 中增加 @babel/plugin-transform-runtime 的配置。

{
    "presets": [],
    "plugins": [
        [
            "@babel/plugin-transform-runtime"
        ]
    ]
}
1
2
3
4
5
6
7
8

🌙 6首屏优化

🌙 6.1CDN分发,减少传输距离

全称:Content Delivery NetworkContent Distribute Network,即内容分发网络。

通过在网络各处放置节点服务器,这些节点之间会动态的互相传输内容,CDN系统能够根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。

目的是就近取得所需内容,解决Internet网络拥挤的状况,提高用户访问网站的响应速度。

🌙 6.2后端数据缓存

数据库查询缓存是可以设置缓存的,这个对于处于高频率的请求很有用。浏览器一般不会对content-type: application/json的接口进行缓存,所以有时需要我们手动地为接口设置缓存。

比如一个用户的签到状态,它的缓存时间可以设置到明天之前。

🌙 6.3前端的资源动态加载

  • 路由懒加载

react路由懒加载和vue路由懒加载 (opens new window)

web 前端图片懒加载实现原理 (opens new window)

  • 图片预加载 HTML5中有原生的预加载属性,名为prefetchprerender:
<!--IE11及以上,兼容性更好-->
<link rel="prefetch" href="(url)">
<!--prefetcher:浏览器会在后台(页面不可见)的位置预加载,相比prefetch,兼容性要差一些-->
<link rel="prefetcher" href="(url)">
1
2
3
4

张鑫旭:基于用户行为的图片等资源预加载 (opens new window)

🌙 6.4减少请求的数量

🌙 6.5使用http压缩,GZIP

  • 验证是否开启GZIP:
 curl -H "Accept-Encoding: gzip" -I https://juejin.cn/post/6844903825585897485#heading-4
1
  • Koa服务开启GZIP
const compress = require('koa-compress');
const app = module.exports = new Koa();
app.use(compress());
1
2
3
  • webpack开启GZIP:
const CompressionWebpackPlugin = require('compression-webpack-plugin');
plugins.push(
    new CompressionWebpackPlugin({
        asset: '[path].gz[query]',// 目标文件名
        algorithm: 'gzip',// 使用gzip压缩
        test: new RegExp(
            '\\.(js|css)$' // 压缩 js 与 css
        ),
        threshold: 10240,// 资源文件大于10240B=10kB时会被压缩
        minRatio: 0.8 // 最小压缩比达到0.8时才会被压缩
    })
);
1
2
3
4
5
6
7
8
9
10
11
12

更进一步,使用更先进的压缩算法,进一步压缩——Brotli (opens new window)(Brotli兼容性差 (opens new window))

前端性能优化之gzip (opens new window)

启用 Brotli 压缩算法,对比 Gzip 压缩 CDN 流量再减少 20% (opens new window)

🌙 6.6页面使用骨架屏

用css提前占好位置,当资源加载完成即可填充,减少页面的回流与重绘,同时还能给用户最直接的反馈。 react-placeholder (opens new window)

使用css伪类:只要css就能实现的骨架屏方案 (opens new window)

CSS3- Loader & Spinners (opens new window)

🌙 6.7使用ssr渲染

🌙 6.8使用async和defer

🌙 6.9合理选择图片格式

AVIF has landed (opens new window)

<picture>  
<!--If this type is supported, use this-->
  <source type="image/avif" srcset="snow.avif">  
<!--…else this-->
  <img alt="Hut in the snow" src="snow.jpg">
</picture>
1
2
3
4
5
6

🌙 6.10 使用HTTP缓存

使用 HTTP 缓存:Etag, Last-Modified 与 Cache-Control (opens new window)

🌙 6.11 浏览器缓存(localStorage)

localstorage 必知必会 (opens new window)

使用 localStorage 的最佳实践 (opens new window)

A Simple TODO list using HTML5 WebDatabases (opens new window)

兼容性:支持IE8+ (opens new window)

// 获取指定key本地存储的值
localStorage.getItem(key)
// 将value存储到key字段
localStorage.setItem(key,value)
//删除指定key本地存储的值
localStorage.removeItem(key) 
1
2
3
4
5
6

cookie最大的缺陷是在每一次HTTP请求中都会携带所有符合规则的cookie数据(最大4k),这会增加请求响应时间,特别是XHR请求.

在HTML5中使用sessionStoragelocalStorage(最大5M)代替cookie是更好的做法:

// if localStorage is present, use that
if (('localStorage' in window) && window.localStorage !== null) {

  // easy object property API
  localStorage.wishlist = '["unicorn", "Narwhal", "deathbear"]';

} else {

  // without sessionStorage we'll have to use a far-future cookie
  // with document.cookie's awkward API
  var date = new Date();
  date.setTime(date.getTime() + (365 * 24 * 60 * 60 * 1000));
  var expires = date.toGMTString();
  var cookiestr = 'wishlist=["unicorn", "Narwhal", "deathbear"];' +
                  ' expires=' + expires + '; path=/';
  document.cookie = cookiestr;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

🌙 小结

web性能优化是一个系统的工程,需要各方面一起配合。大概从两个方向入手:

  • 减少资源体积

  • 控制请求数

    • DNS解析
    • 资源合并(js、css、html)
    • 图片合并(雪碧图、base64)
    • 减少接口数量
    • 并行加载(使用async/defer、ajax)
    • 合理使用缓存(http缓存、localstorage)
    • 懒加载(图片、路由、组件)和预加载
    • 静态资源CDN
    • 使用http2
    • 使用websocket

前端性能优化 24 条建议 (opens new window)

前端性能优化 (opens new window)

大话WEB前端性能优化基本套路 (opens new window)

[译]让web app更快的HTML5最佳实践 (opens new window)

前端优化大全-你想要的我全都有 (opens new window)

前端性能优化 24 条建议(2020) (opens new window)

带你深度解锁webpack系列(优化篇) (opens new window)

嗨,送你一张Web性能优化地图 (opens new window)

🌙 7 实践

🌙 7.1 node使用HTTP2 (opens new window)

  • http1 sever示例:
const express = require('express');
const logger = require('morgan');
const compression = require('compression');
const delayConfig = require('./delayConfig');
const app = express();
app.use(logger('dev'));
app.use((req, res, next) => {
    let url = req.url;
    const delay = delayConfig[url];
    if (delay) {
        setTimeout(next, delay);
    } else {
        next();
    }

});
//启用gzip压缩
app.use(compression());
app.use(express.static('public'));
app.listen(80, () => console.log('服务器已经在80端口上启动了....'));

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • http2 sever示例:

生成公钥和私钥

openssl req -x509 -newkey rsa:2048 -nodes -sha256 -keyout localhost-privkey.pem -out localhost-cert.pem
1
// 使用http2
const http2 = require('http2');
const fs = require('fs');
const path = require('path');

const server = http2.createSecureServer({
  key: fs.readFileSync(path.resolve(__dirname, "certificate/localhost-privkey.pem")),
  cert: fs.readFileSync(path.resolve(__dirname, "certificate/localhost-cert.pem"))
});

server.on('error', (err) => console.error(err));

server.on('stream', (stream, headers) => {
    // stream is a Duplex
    stream.respond({
        'content-type': 'text/html; charset=utf-8',
        ':status': 200
    });
    stream.end('<h1>Hello World</h1>');
});

server.listen(8888);
console.log('server is listening on https://localhost:8888')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

🌙 7.1测试网页性能FP、FCP、LCP、FIP、CLS

  • 使用lighthouse输出:
lighthouse https://m.jd.com --locale zh --quiet --chrome-flags="--headless"  --only-categories=performance
1
  • 使用chrome浏览器开发者工具lighthouse

  • 使用js输出:

(function (ready) {
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        ready();
    } else {
        document.addEventListener('readystatechange', () => {
            if (document.readyState === 'complete') {
                ready();
            }
        });
    }
})(function perf() {
    const data = {
        FP: 0,//首次绘制
        FCP: 0,//首次内容绘制
        LCP: 0,//最大内容绘制
        FIP: 0, //用户首次交互的延迟
        CLS: 0,//总计布局偏移
    }
    //如果观察者观察到了指定类型的性能条目,就执行回调
    new PerformanceObserver(function (entryList) {
        let entries = entryList.getEntries();
        entries.forEach(entry => {
            if (entry.name === 'first-paint') {
                // 手次绘制的开始时间
                data.FP = entry.startTime;
                console.log('记录FP', data.FP);
            } else if (entry.name === 'first-contentful-paint') {
                data.FCP = entry.startTime;
                console.log('记录FCP', data.FCP);
            }
        });

    }).observe({ type: 'paint', buffered: true });

    new PerformanceObserver(function (entryList) {
        let entries = entryList.getEntries();
        entries.forEach(entry => {
            if (entry.startTime > data.LCP) {
                console.log('记录LCP', (data.LCP = entry.startTime));
            }
        });

    }).observe({ type: 'largest-contentful-paint', buffered: true });

    new PerformanceObserver(function (entryList) {
        let entries = entryList.getEntries();
        entries.forEach(entry => {
            //首次用户交互  开始处理的时间减去 开始的交互的时间 就是首次交互延迟的时间
            const FID = entry.processingStart - entry.startTime;
            console.log('FID', FID, entry);
        });

    }).observe({ type: 'first-input', buffered: true });

    new PerformanceObserver(function (entryList) {
        let entries = entryList.getEntries();
        entries.forEach(entry => {
            data.CLS += entry.value;
            console.log('CLS:', data.CLS);
        });

    }).observe({ type: 'layout-shift', buffered: true });
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64