Web性能的计算方式与优化方案(二)

常见优化手段

通过上面《Web性能的计算方式与优化方案(一)》几个小节的学习,我们了解到如果我们想最终在浏览器显示我们所期望的页面和交互效果,那我们首先需要的是我们应用代码、资源、脚本一切准备好,才后续页面的渲染和展示,于是缩短加载的时间,也是我们性能优化的表现,这里我们给大家介绍几个常用的优化手段。

异步加载

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

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

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

async和defer

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

javascript 复制代码
// 面试经常问: script标签的defer和async的区别?

// defer要等到html解析完成之后执行脚本
<script src="main.js" defer></script>
// async异步加载脚本后便会执行脚本
<script src="main.js" async></script>

动态添加script标签

ini 复制代码
// js代码中动态添加script标签,并将其插入页面
const script = document.createElement("script");
script.src = "a.js"; 
document.head.appendChild(script);

通过XHR异步加载js

javascript 复制代码
// 面试经常问: 谈谈JS中的 XMLHttpRequest 对象的理解?


var xhr = new XMLHttpRequest()
/*
第一个参数是请求类型
第二个参数是请求的URL
第三个参数是是否为异步请求
*/
xhr.open('get', '/getUser', true) // true代表我们需要异步加载该脚本
xhr.·setRequestHeader('testHeader', '1111') // 自定义Header
xhr.send(null); // 参数为请求主体发送的数据,为必填项,当不需要发送数据时,使用null
xhr.onreadyStateChange = function () {
  if (xhr.readystate === 4) {
    // 面试经常问: 说出你知道的哪些HTTP状态码? 
    if (xhr.status === 304 || (xhr.status >= 200 && xhr.status < 300)) {
      console.log('成功, result: ', xhr.responseText);
    } else {
      console.log('错误, errCode:', xhr.status);
    }
  }
}

按需打包与按需加载

随着Webpack等构建工具的能力越来越强,开发者在构建阶段可以随心所欲地打造项目流程,与此同时按需加载和按需打包的技术曝光度也越来越高,甚至决定着工程化构建的结果,直接影响应用的性能优化。

两者的概念:

  • 按需打包表示的是针对第三方依赖库及业务模块。只打包真正在运行时可能会用到的代码。

  • 按需加载表示的是代码模块在交互的时候需要动态导入。

按需打包

按需打包一般通过两种方法来实现:

1、使用ES Module支持的Tree Shaking方案,使用构建工具时候完成按需打包。

我们看一下这种场景:

javascript 复制代码
import { Button } from 'antd';

// 假设我们的业务使用了Button组件,同时该组件库没有提供ES Module版本,
// 那么这样的引用会导致最终打包的代码是所有antd导出的内容,这样会大大增加代码的体积

// 但是如果我们组件库提供了ES Module版本(静态分析能力),并且开启了Tree Shaking功能,
// 那么我们就可以通过"摇树"特性
// 将不会被使用的代码在构建阶段移除。

正确使用Tree Shaking的姿势:

antd组件库

json 复制代码
// package.json
{
    // ...
  "main": "lib/index.js", // 暴露CommonJS规范代码lib/index.js
  "module": "es/index.js", // 非package.json标准字段,打包工具专用字段,指定符合ESM规范的入口文件
  
  // 副作用配置字段,告诉打包工具遇到sideEffects匹配到的资源,均为无副作用的模块呢?
  "sideEffects": [
    "*.css",
    " expample.js"
  ],
}
arduino 复制代码
  // 啥叫作副作用模块
  // expample.js
  
 const b = 2 
 export const a = 1
  
 console.log(b)
  
  

项目:

Tree Shaking一般与Babel搭配使用,需要在项目里面配置Babel,因为Babel默认会把ESM规范打包成CommonJs代码,所以需要通过配置babel-preset-env#moudles编译降级

css 复制代码
production: {
  presets: [
      '@babel/preset-env',
      {
          modules: false
      }
  ]
}

webpack4.0以上在mode为production的时候会自动开启Tree Shaking,实际就是依赖了、UglifyJS等压缩插件,默认配置

arduino 复制代码
const config = {
    mode: 'production',
        optimization: {
            // 三类标记:  
            // used export: 被使用过的export会这样标记    
            // unused ha  by rmony export: 没有被使用过的export被这样标记   
            // harmony import: 所有import会被这样标记
            
            usedExports: true, // 使用usedExports进行标记
            minimizer: {
                new TerserPlugin({...}) // 支持删除未引用代码的压缩器
            }
        }
}

2、使用以babel-plugin-import为主的Babel插件完成按需打包。

yaml 复制代码
[
  {
    libraryName: 'antd',
    libraryDirectory: 'lib', // default: lib
    style: true
  },
  {
    libraryName: 'antd'
  }
];
javascript 复制代码
import { TimePicker } from "antd"
↓ ↓ ↓ ↓ ↓ ↓
var _button = require('antd/lib/time-picker');

按需加载

如何才能动态地按需导入模块呢?

动态导入import(module) 方法加载模块并返回一个 promise,该 promise resolve 为一个包含其所有导出的模块对象。我们可以在代码中的任意位置调用这个表达式。不兼容浏览器,可以用Babel进行转换(@babel/plugin-syntax-dynamic-import )

javascript 复制代码
// say.js

export function hi() {
  alert(`你好`);
}

export function bye() {
  alert(`拜拜`);
}

export default function() {
  alert("默认到处");
}

{
    hi: () => {},
    bye: () => {},
    default:  xxxx
}
xml 复制代码
<!doctype html>
<script>async function load() {
    let say = await import('./say.js');
    say.hi(); // 你好
    say.bye(); // 拜拜
    say.default(); // 默认导出
</script>
<button onclick="load()">Click me</button>

如果让你手写一个不考虑兼容性的import(module)方法,你会怎么写?可以看下以下Function-like

ini 复制代码
        // 利用ES6模块化来实现
        const dynamicImport = (url) => {
            return new Promise((resolve, reject) => {
                // 创建script标签
                const script = document.createElement("script");
                const tempGlobal = "__tempModuleVariable" + Math.random().toString(32).substring(2);
                // 通过设置 type="module",告诉浏览器该脚本是一个 ES6 模块,需要按照模块规范进行导入和导出
                script.type = "module";
                script.crossorigin="anonymous"; // 跨域
                script.textContent = `import * as m from "${url}"; window.${tempGlobal} = m;`;
                // load 回调
                script.onload = () => {
                    resolve(window[tempGlobal]);
                    delete window[tempGlobal];
                    script.remove();
                };
                
                // error回调
                script.onerror = () => {
                    reject(new Error(`Fail to load module script with URL: ${url}`));
                    delete window[tempGlobal];
                    script.remove();
                };
        
                document.documentElement.appendChild(script);
            });
        }
        
        // const dynamicImport = (url) => {
        //     return new Promise((resolve, reject) => {
        //         const script = document.createElement("script");
        //         script.type = "module";
        //         script.src = url;

        //         script.onload = () => {
        //             resolve(script.module);
        //             script.remove();
        //         };
                
        //         script.onerror = () => {
        //             reject(new Error("Failed to load module script with URL " + url));
        //             script.remove();
        //         };
                
        //         document.documentElement.appendChild(script);
        //     });
        // }

Bigpipe技术

BigPipe,这是一个十多年前的技术,这里简单介绍一下,

BigPipe 最早上 FaceBook 用来提升自家网站性能的一个秘密武器。其核心思想在于将页面分成若干小的构件,我们称之为 pagelet。每一个构件之间并行执行。

那么 BigPipe 做了什么?和传统方式有什么不同呢?我们知道浏览器处理我们的 HTML 文档以及其中包含的 CSS,JS 等资源的时候是从上到下串行执行的。如果我们把浏览器处理的过程划分为若干阶段(stage),那么这些阶段之间有着明显的时间先后关系。那么我们能不能将其并行化,从而减少时间呢?这就是 BigPipe 的基本思想。下面代码介绍了一个大概的实现:

xml 复制代码
<!DOCTYPE html>
<html>
  <head>
    <script>
      window.HandleBigPipe = {
        render(selector, content) {
          document.querySelector(selector).innerHTML = content;
        }
      };
    </script>
  </head>
  <body>
    <div id="pagelet1"></div>
    <div id="pagelet2"></div>
    <div id="pagelet3"></div>
  </body>
</html>
javascript 复制代码
const app = require('express')();
const fs = require('fs');

// 模拟真实场景
function wirteChunk(content, delay, res) {
    return new Promise(r => {
        setTimeout(function() {
            res.write(content);
        delay);
    })
}

app.get('/', function (req, res) {
  // 为了简化代码,直接同步读。 强烈不建议生产环境这么做!
  res.write(fs.readFileSync(__dirname + "/home.html").toString());

  const p1 = wirteChunk('<script>HandleBigPipe.render("#pagelet1","hello");</script>', 1000)
  const p2 = wirteChunk('<script>HandleBigPipe.render("#pagelet2","word");</script>', 2000)
  const p3 = wirteChunk('<script>HandleBigPipe.render("#pagelet3","!");</script>', 3000)

  Promise.all([p1, p2, p3]).then(res.end)

});

app.listen(3000);

浏览器原理

前面我们学习的网页性能指标一节中,我们了解了一个重要的指标叫做"FP ",页面加载到首次开始绘制的时长,而影响该指标其中有一个重要因素就是是网络加载速度, 所以我们要想更好地优化 Web 页面的加载速度,学习浏览器工作原理相关知识是有必要的,其中涉及了网络、操作系统、Web等一系列知识,可以让你更清楚如何去优化 Web 性能,或者能更轻松地定位 Web 问题。

浏览器架构演进

单进程浏览器由于进程与线程之间几个特点,显得不稳定,不流畅,面试的时候会经常会问:什么是进程?什么是线程?

进程: 一个程序的运行实例

线程: 进程中执行运算最小单位

1. 进程中的任意一线程执行出错,都会导致整个进程的崩溃。

2. 线程之间共享进程中的数据。

3. 当一个进程关闭之后, 操作系统 会回收进程所占用的 内存

4. 进程之间的内容相互隔离

同时也根据进程与线程之间几个特点,演变出了多进程浏览器

浏览器进程,主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。

渲染进程,核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页。

GPU 进程, 使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制。

网络进程,主要负责页面的网络资源加载。

插件进程,主要是负责插件的运行。

浏览器工作

了解了浏览器架构演进后,我们对浏览器有了个初步的了解,现在我们通过回答怎么一道经典的面试题" 在浏览器中从输入 URL 到页面展示,这中间发生了什么? "来展开说说浏览器的工作原理。

数据包传输

在详细展开之前,我们得先了解一下互联网数据是如何在不同计算机之间传递的? 怎么就一个大的页面文件会能被完整地送达到我们的浏览器上的?

首先一个的大的数据文件不是一次性传输的,是会被拆成一个个小的数据包来传输的。所以我们需要了解的是数据包,是在两台主机是如何去传输的?我们看看下图:

http请求发起与响应

上面我了解TCP/IP 协议是如何完成数据完整传输的设计思想,回到我们的问题,在浏览器中从输入 URL 到页面展示过程中,前半部分我们要关注的就是怎么将页面请求回来?

HTTP 是一种允许浏览器向服务器获取资源的协议,是 Web 的基础,通常由浏览器发起请求,用来获取不同类型的文件,例如 HTML 文件、CSS 文件、JavaScript 文件、图片、视频等,浏览器使用 HTTP 协议作为应用层协议,用来封装请求的文本信息;建立好TCP连接,我们常说的TCP三次握手,并使用 TCP/IP 作传输层协议将它发到网络上。

http请求 发起

  1. 构建请求

首先,浏览器构建请求行信息,构建好后,浏览器准备发起网络请求。

GET /index.html HTTP1.1

curl -v  www.juejin.cn
  1. 查找是否有缓存

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

关于浏览器缓存,我们也是不陌生的:

面试经常会问: 浏览器的缓存:强缓存、协商缓存的区别?

  1. 准备 IP 地址和端口

我们在浏览器输入的是URL,这里浏览器会通过请求 DNS (域名系统)返回域名对应的 IP,这里有一点需要关注的是浏览器还提供了 DNS 客户端缓存策略,如果某个域名已经解析过了,那么浏览器会缓存解析的结果,以供下次查询时直接使用,这样也会减少一次网络请求。

HTTP协议默认URL, 端口号是80 https端口号哪一个?

上一小节,我们从数据包传输过程,我们明白建立TCP连接, 准备IP 地址和端口是必要且提前的。

  1. 等待 TCP 队列

Chrome 有个机制,同一个域名同时最多只能建立 6 个 TCP 连接, 如果 当前已经开启6个TCP连接正在进行数据包传输了,那么该http请求就需要等待,等待正在进行的请求结束。http1.1 6个TCP连接, http2几个TCP连接?

  1. 建立 TCP 连接
  1. 发送 HTTP 请求

一旦建立了 TCP 连接,浏览器就可以和服务器进行通信了

HTTP 请求里面会有请求行、请求头、请求体(最终需要传递的数据)

http请求 响应

  1. 返回请求

接下来,服务器会根据浏览器的请求信息来准备相应的内容。"200"为成功的状态码,"404"为没有找到页面的状态码,对应的还有许多http状态码

curl -v  www.juejin.cn
  1. 断开TCP连接

一旦服务器向客户端返回了请求数据,它就要关闭 TCP 连接,但是在HTTP1.1, 如果头信息有**Connection:Keep-Alive**, 那我们会继续复用这个TCP连接,也就节省了建立TCP连接的时间。

  1. 重定向

响应行返回的状态码是 301,状态 301 就是告诉浏览器,我需要重定向到另外一个网址,而需要重定向的网址正是包含在响应头的 Location 字段中,接下来,浏览器获取 Location 字段中的地址,并使用该地址重新导航,这就是一个完整重定向的执行流程

makefile 复制代码
C:\Users\shiyi>curl -I www.juejin.cn
HTTP/1.1 301 Moved Permanently
Date: Fri, 03 Nov 2023 06:58:17 GMT
Server: Varnish
X-Varnish: 6539601
x-cache: synth synth
Location: https://www.juejin.com/
Content-Length: 0
Connection: keep-alive

浏览器页面渲染

通过上一节http请求的发起和响应,及之前我们学习的多进程浏览器架构演进,我们看看多进程配合下是如何展示从URL输入到显示的过程。

  • 首先,浏览器进程接收到用户输入的 URL 请求,浏览器进程便将该 URL 转发给网络进程。
  • 然后,在网络进程中发起真正的 URL 请求。
  • 网络进程接收到了响应头数据,便解析响应头数据,并将数据转发给浏览器进程。
  • 浏览器进程接收到网络进程的响应头数据之后,发送"提交文档 (CommitNavigation) "消息到渲染进程;
  • 渲染进程接收到 "提交文档" 的消息之后,便开始准备接收 HTML 数据,接收数据的方式是直接和网络进程建立数据管道;
  • 最后渲染进程会向浏览器进程 "确认提交" ,这是告诉浏览器进程:"已经准备好接受和解析页面数据了"。
  • 浏览器进程接收到渲染进程 "确认提交" 的消息之后,便开始移除之前旧的文档,然后更新浏览器进程中的页面状态。

好了,可以看出渲染进程主要负责整个页面的渲染流程,按顺序额分别是构建Dom树、样式计算、布局阶段、分层、绘制、分块、光栅化、合成和显示,我们用一张图来总结一下整个渲染流程:

  • 分层(Layout)

页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree

如访问: zhaowaedu.com/#/

  • 绘制(Paint)

会形成绘制列表,然后commit 给合成线程,合成线程会先根据可视窗口分块( tile ,优先绘制可视窗口中的图块,图块需要转成位图,便由光栅化去做处理,渲染引擎维护这一个光栅化线程池来进行执行,而光栅化线程池又会调用GPU来加速生成位图,此时此刻GPU进程也介入进来,等所有光栅化后,会调取一个总的图层绘制命令,将页面内容绘制到内容,再从内存读出,浏览器显示。

PWA的中文名叫做渐进式网页应用 ,它的诞生了是为了在用户体验和用户留存两方面提供了更好的解决方案,将 WebApp 各自的优势融合在一起:渐进式、可响应、可离线、实现类似 App 的交互、即时更新、安全、可以被搜索引擎检索、可推送、可安装、可链接。

谷歌为什么推崇这一门技术?

多项技术组成

App Manifest

arduino 复制代码
// 声明manifest.json,在html引入,这样我们在高版本浏览器访问我们的网站,
// 可以将我们的网站入口形成一个图标放置在浏览器的主屏幕中
<link  rel="mainifest" href="./manifest.json" />

manifest.json格式:

json 复制代码
{

   // 必须的字段3个

   "name": "MyExtension", // 扩展名称

   "version": "1.0", // 版本。由1到4个整数构成。多个整数间用"."隔开

   "manifest_version": 2, // manifest文件版本号。Chrome18开始必须为2

   // 建议提供的字段3个

   "description": "", // 描述。132个字符以内

   "icons": {

      "16": "image/icon-16.png",

      "48": "image/icon-48.png",

      "128": "image/icon-128.png"

   }, //扩展图标。推荐大小16,48,128

   "default_locale": "en", // 国际化

   // 以下字段多选一,或者都不提供

   "browser_action": {

      "default_icon": "image/icon-128.png",

      "default_title": "My Test",

      "default_popup": "html/browser.html"

   }, //地址栏右侧图标管理。含图标及弹出页面的设置等

   "page_action": {

      "default_icon": "image/icon-48.png",

      "default_title": "My Test",

      "default_popup": "html/page.html"

   }, //地址栏最后附加图标。含图标及行为等

   "theme": {}, // 主题,用于更改整个浏览器的外观

   "app": {}, // 指定扩展需要跳转到的URL

   // 根据需要提供

   "background": {

      "scripts": [

         "lib/jquery-3.3.1.min.js",

         "js/background.js"

      ],

      "page": "html/background.html"

   }, // 指定扩展进程的background运行环境

   "chrome_url_overrides": {

      "pageToOverride": "html/overrides.html"

   }, //替换页面

   "content_scripts": [{

      "matches": ["https://www.zhaowaedu.com/*"],

      "css": ["css/styles.css"],

      "js": ["lib/jquery-2.8.5.min.js", "js/content.js"]

   }], // 指定在web页面运行的脚本

   "content_security_policy": "", // 安全策略

   "file_browser_handlers": [],

   "homepage_url": "http://xxx", // 扩展的官方主页

   "incognito": "spanning", // 或"split"

   "intents": {}, // 用户操作意图描述

   "key": "", // 扩展唯一标识。不需要人为指定

   "minimum_chrome_version": "1.0", // 扩展所需chrome的最小版本

   "nacl_modules": [], // 消息与本地处理模块映射

   "offline_enabled": true, // 是否允许脱机运行

   "omnibox": {

      "keyword": "myKey"

   }, //ominbox即地址栏。用于响应地址栏的输入事件

   "options_page": "aFile.html", // 选项页。用于在扩展管理页面跳转到选项设置

   "permissions": [

      "https://www.baidu.com/*",

      "background",

      "tabs"

   ], //权限

   "plugins": [{

      "path": "extension_plugin.dll",

      "public": true

   }], // 扩展。可调用第三方扩展

   "requirements": {}, // 指定所需要的特殊技术。目前只支持"3D"

   "update_url": "http://path/to/updateInfo.xml", // 自动升级

   "web_accessible_resources": [] // 指定资源路径,为String数组

}

Service Worker

所谓的Service Worker,本质上也是浏览器缓存资源用的,一个服务器与浏览器之间的中间人角色,如果网站中注册了service worker那么它可以拦截当前网站所有的请求,进行判断(需要编写相应的判断程序),如果需要向服务器发起请求的就转给服务器,如果可以直接使用缓存的就直接返回缓存不再转给服务器。从而大大提高浏览体验。

特点:单独web worker独立线程,在这里跑程序,不影响主线程执行任务

可以访问cache和indexDB,可以管理自己的缓存

事件驱动的,具有生命周期

必须是https协议

最简单的案例代码:

1、主入口html注册service worker

javascript 复制代码
/* 判断当前浏览器是否支持serviceWorker */
    if ('serviceWorker' in navigator) {
        /* 当页面加载完成就创建一个serviceWorker */
        window.addEventListener('load', function () {
            /* 编写serviceWorker.js文件,注册我们的serviceWorker */
            navigator.serviceWorker.register('./serviceWorker.js')
                .then(function (registration) {
 
                    console.log('ServiceWorker 注册成功,范围: ', registration.scope);
                })
                .catch(function (err) {
 
                    console.log('ServiceWorker 注册失败: ', err);
                });
        });
    }

2、serviceWorker.js 安装与监听

javascript 复制代码
/* 监听安装事件,install 事件一般是被用来设置你的浏览器的离线缓存逻辑 */
this.addEventListener('install', function (event) {
    
    /* 通过这个方法可以防止缓存未完成,就关闭serviceWorker */
    event.waitUntil(
        /* 创建一个名叫V1的缓存版本 */
        caches.open('v1').then(function (cache) {
            /* 指定要缓存的内容,地址为相对于跟域名的访问路径 */
            return cache.addAll([
                './index.html',
                './a.css',
                './b.css'
            ]);
        })
    );
});

// 删除旧cache
function deletePreCaches() {
    // ...
}

//service worker激活阶段,说明上一sw已失效
this.addEventListener('activate', function(event) {
    
    event.waitUntil(
        // 遍历 caches 里所有缓存的 keys 值
        caches.keys().then(deletePreCaches)
    );
});

/* 注册fetch事件,拦截全站的请求 */
this.addEventListener('fetch', function(event) {
    event.respondWith(
      // magic goes here
        
        /* 在缓存中匹配对应请求资源直接返回 */
      caches.match(event.request)
    );
  });

大家感兴趣的话,可以参照谷歌官方制作自己的第一个service worker应用, developers.google.com/codelabs/pw...

Web Push

PWA中的另一个重要功能------消息推送与提醒(Push & Notification)。这个能力让我们可以从服务端向用户推送各类消息并引导用户触发相应交互

Web Push全流程:

  • Ask Permission:这一步不再上图的流程中,这其实是浏览器中的策略。浏览器会询问用户是否允许通知,只有在用户允许后,才能进行后面的操作。

  • Subscribe:浏览器(客户端)需要向Push Service发起订阅(subscribe),订阅后会得到一个PushSubscription对象

  • Monitor:订阅操作会和Push Service进行通信,生成相应的订阅信息,Push Service会维护相应信息,并基于此保持与客户端的联系;

  • Distribute Push Resource:浏览器订阅完成后,会获取订阅的相关信息(存在于PushSubscription对象中),我们需要将这些信息发送到自己的服务端,在服务端进行保存。

  • Push Message阶段一:我们的服务端需要推送消息时,不直接和客户端交互,而是通过Web Push协议,将相关信息通知Push Service

  • Push Message阶段二:Push Service收到消息,通过校验后,基于其维护的客户端信息,将消息推送给订阅了的客户端;

  • 最后,客户端收到消息,完成整个推送过程。

感兴趣的话,大家可以去了解一下消息推送与提醒这两个功能------Push API 和 Notification API

React性能优化常见策略

【render过程】避免不必要的Render

  • 类组件跳过没有必要的组件更新, 对应的技巧手段:PureComponent、React.memo、shouldComponentUpdate。

PureComponent 是对类组件的 Props 和 State 进行浅比较

React.memo是对函数组件的 Props 进行浅比较

shouldComponentUpdate是React类组件的钩子,在该钩子函数我们可以对前后props进行深比对,返回false可以禁止更新组件,我们可以手动控制组件的更新

  • Hook的useMemo、useCallback 获得稳定的 Props 值

传给子组件的派生状态或函数,每次都是新的引用,这样会导致子组件重新刷新

scss 复制代码
import { useCallback, useState, useMemo } from 'react';

const [count, setCount] = useState(0);
// 保证函数引用是一样的,在将该函数作为props往下传递给其他组件的时候,不会导致
// 其他组件像PureComponent、shouldComponentUpdate、React.memo等相关优化失效
// const oldFunc = () => setCount(count => count + 1)
const newFunc useCallback(() => setCount(count => count + 1), [])

// useMemo与useCallback 几乎是99%相似,只是useMemo一般用于密集型计算大的一些缓存,
// 它得到的是函数执行的结果
  const calcValue = useMemo(() => {
    return Array(100000).fill('').map(v => /*耗时计算*/ v);
  }, [count]);
  • state状态下沉,减小影响范围

如果一个P组件,它有4个子组件ABCD,本身有个状态state p, 该状态只影响到AB ,那么我们可以把AB组件进行封装, state p 维护里面,那么state p变化了,也不会影响到CD组件的渲染

  • redux、 React 上下文ContextAPI 跳过中间组件Render
javascript 复制代码
import ReactDOM from "react-dom";

import { createContext, useState, useContext, useMemo } from "react";

const Context = createContext({ val: 0 });

const MyProvider = ({ children }) => {
  const [val, setVal] = useState(0);
  const handleClick = () => {
    setVal(val + 1);
  };

  const value = useMemo(() => {
    return {
      val: val
    };
  }, [val]);

  return (
    <Context.Provider value={value}>
      {children}
      <button onClick={handleClick}>context change</button>
    </Context.Provider>
  );
};

const useVal = () => useContext(Context);

const Child1 = () => {
  const { val } = useVal();
  console.log("Child1重新渲染", val);

  return <div>Child1</div>;
};

const Child2 = () => {
  console.log("Child2只渲染一次");
  return <div>Child2</div>;
};

function App() {
  return (
    <MyProvider>
      <Child1 />
      <Child2 />
    </MyProvider>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App/>, rootElement);
  • 避免使用内联函数
scala 复制代码
import React from "react";

export default class InlineFunctionComponent extends React.Component {
 componentDidmount() {
     this.xxx.bind(this)
 }
  render() {
    return (
      <div>
        <h1>你好呀</h1>
        <input type="button" onClick={(e) => { this.setState({value: e.target.value}) }} value="Click For Inline Function" />
      </div>
    )
  }
}
  • 使用 Immutable,减少渲染的次数

【Diff 过程】减少比对

  • 列表项使用 key 属性, **React 官方推荐**将每项数据的 ID 作为组件的 key

那我如果使用索引值index作为key,为啥不推荐? 面试题

xml 复制代码
// 无用更新

<!-- 更新前 -->
<li key="0">Tom</li>
<li key="1">Sam</li>
<li key="2">Ben</li>
<li key="3">Pam</li>

<!-- 删除后更新 -->
<li key="0">Sam</li>
<li key="1">Ben</li>
<li key="2">Pam</li>
xml 复制代码
// 输入错乱

<!-- 更新前 -->
<input key="0" value="1" id="id1"/> 
<input key="1" value="2" id="id2"/>
<input key="3" value="3" id="id3"/>
<input key="4" value="4" id="id4"/>

<!-- 删除后更新 -->
<input key="1" value="1" id="id2"/>
<input key="3" value="2" id="id3"/>
<input key="4" value="3" id="id4"/>

其他优化

  • 组件懒加载,可以是通过 Webpack 的动态导入和 React.lazy 方法
javascript 复制代码
import { lazy, Suspense, Component } from "react"

const Com = lazy(() => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.5) {
        reject(new Error("error"))
      } else {
        resolve(import("./Component"))
      }
    }, 1000)
  })
})

// ...

<Suspense fallback="加载...">
<Com />
</Suspense>
  • 虚拟滚动,react-window 和 react-virtualized, 常见面试题是:给你10000条数据一次性展示,怎么才不会卡,虚拟滚动的原理?
  • debounce、throttle 优化触发的回调,如input组件onChange防抖 Lodash
  • 善用缓存,如上面用的useMemo,可以做一些耗时计算并保持引用不变,减少重新渲染
  • ...

Vue性能优化常见策略

可以从代码分割、服务端渲染、组件缓存、长列表优化等角度去分析Vue性能优化常见的策略。

  • 最常见的路由懒加载:有效拆分App体积大小,访问时异步加载
css 复制代码
const router = createRouter({
  routes: [
    // 借助webpack的import()实现异步组件
    { path: '/foo', component: () => import('./Foo.vue') }
  ]
})
  • keep-alive缓存页面:避免重复创建组件实例,且能保留缓存组件状态
ruby 复制代码
  <keep-alive>
    <component :is="Component"></component>
  </keep-alive>
  • 使用v-show复用DOM:避免重复创建组件
xml 复制代码
<template>
  <div class="cell">
    <!-- 这种情况用v-show复用DOM,比v-if效果好 -->
    <div v-show="value" class="on">
      <Count :num="10000"/>  display:none
    </div>
    <section v-show="!value" class="off">
      <Count :num="10000"/>
    </section>
  </div>
</template>
  • 不再变化的数据使用v-once
xml 复制代码
<!-- single element -->
<span v-once>This will never change: {{msg}}</span>
<!-- the element have children -->
<div v-once>
  <h1>comment</h1>
  <p>{{msg}}</p>
</div>
<!-- component -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` directive -->
<ul>
  <li v-for="i in list" v-once>{{i}}</li>
</ul>
ini 复制代码
// vue-lazyload
<img v-lazy="/static/img/1.png">
  • 第三方插件按需引入
javascript 复制代码
import { createApp } from 'vue';
import { Button, Select } from 'element-plus';

const app = createApp()
app.use(Button)
app.use(Select)
  • 服务端渲染/静态网站生成:SSR/SSG
  • ...
# 性能优化监控工具
## DevTools-chrome performance

Charles-抓包分析神器

Lighthouse-知名测评⼯具

webpagetest-知名测评⽹站

相关推荐
时清云27 分钟前
【算法】合并两个有序链表
前端·算法·面试
小爱丨同学34 分钟前
宏队列和微队列
前端·javascript
持久的棒棒君1 小时前
ElementUI 2.x 输入框回车后在调用接口进行远程搜索功能
前端·javascript·elementui
2401_857297911 小时前
秋招内推2025-招联金融
java·前端·算法·金融·求职招聘
undefined&&懒洋洋2 小时前
Web和UE5像素流送、通信教程
前端·ue5
大前端爱好者4 小时前
React 19 新特性详解
前端
小程xy4 小时前
react 知识点汇总(非常全面)
前端·javascript·react.js
随云6324 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
随云6324 小时前
WebGL编程指南之进入三维世界
前端·webgl
寻找09之夏5 小时前
【Vue3实战】:用导航守卫拦截未保存的编辑,提升用户体验
前端·vue.js