前端(六):前端性能优化

201 阅读20分钟

启用前端缓存

浏览器缓存

Cookie

  • 可存储数据大小 4KB 左右

  • 只能存字符串类型数据

  • Cookie 的属性

    • expires:Cookie 的过期时间,格式为 GMT 格式的日期字符串。如果不设置,Cookie 会在浏览器关闭时失效。

    • path:Cookie 的作用路径,默认为当前页面路径。

    • domain:Cookie 的作用域名。

    • HttpOnly:如果设置,浏览器将禁止通过 JavaScript 脚本读取或修改这些 Cookie。

    • secure:如果设置,Cookie 将仅通过 HTTPS 协议发送。

  • 不同域名的网站之间默认不能互相访问对方的 Cookie,但可以通过设置 Cookie 的 domain 属性来实现跨域访问(仅限于主域相同子域不同的情况)。

localStorage

  • HTML5 新特性,可存储数据大小 5MB 左右

  • 只能存字符串类型数据

  • API

    • 新增/修改:localStorage.setItem('key', 'value')

    • 读取:localStorage.getItem('key')

    • 删除单个:localStorage.removeItem('key')

    • 删除全部:localStorage.clear()

  • 没有过期时间,关掉浏览器/重启电脑也会存在,除非手动清除缓存。但是可以通过封装方法来实现自定义过期时间,思路是在存储数据的同时,存储一个过期时间,并在每次读取数据时判断该数据是否过期。

  • 严格遵循同源策略

sessionStorage

  • HTML5 新特性,可存储数据大小 5MB 左右

  • 只能存字符串类型数据

  • API

    • 新增/修改:sessionStorage.setItem('key', 'value')

    • 读取:sessionStorage.getItem('key')

    • 删除单个:sessionStorage.removeItem('key')

    • 删除全部:sessionStorage.clear()

  • 会话级别,只在会话期间有效。一旦标签页或窗口被关闭,存储的数据就会消失。

  • 严格遵循同源策略

有了 localStorage 为什么还要使用 Cookie 呢?

  • Cookie 会自动随着每个 HTTP 请求发送到服务器,而 localStorage 不会。这一特性使得 Cookie 非常适合用于身份验证、会话跟踪和状态管理等场景。

  • Cookie 可以通过设置 HttpOnly 和 Secure 属性来提高安全性。

    • HttpOnly 可以用来防止跨站脚本(XSS)

    • Secure 设置仅在 HTTPS 协议上发送

  • Cookie 兼容性更好

  • 服务器可以发送和修改 Cookie;而 localStorage 和 sessionStorage 完全由客户端控制,服务器无法直接访问或修改它们。

HTTP 缓存

是在 HTTP 协议中定义的一种数据缓存机制,通过在客户端(如浏览器)或代理服务器(如 nginx)中存储响应数据,以便在后续请求中复用这些数据

HTTP 缓存主要解决哪些问题?

  • 减少不必要的网络传输

  • 减低延迟、提高响应速度

  • 减少服务器负载

  • 可以离线预览

缺点就是会占用内存。

HTTP 缓存又分为两种缓存,强制缓存协商缓存

强制缓存

如果浏览器判断请求的目标资源有效命中强缓存,则可以直接从内存中读取目标资源,无需与服务器做任何通讯。

Expires

在以前,我们通常会使用响应头的 Expires 字段去实现强缓存:

public class CacheControlServlet extends HttpServlet {  
  
    @Override  
    protected void doGet(HttpServletRequest request, HttpServletResponse response)  
            throws ServletException, IOException {  
  
        // 设置内容类型  
        response.setContentType("text/html;charset=UTF-8");  
  
        // 创建一个表示资源过期时间的Date对象  
        // 例如,设置资源在1小时后过期  
        Date expires = new Date(System.currentTimeMillis() + 1000 * 60 * 60); // 1小时后的时间  
  
        // 设置Expires头部  
        response.setDateHeader("Expires", expires.getTime());  
  
        // 你可以在这里添加更多的逻辑来生成响应内容  
  
        // 响应完成  
    }  
  
    // 注意:doPost等其他方法也可以根据需要被重写  
}

Expires 判断强缓存是否过期的机制是:获取本地时间戳,与资源文件中的 Expires 字段的时间做比较,在时间范围内,则从内存(或磁盘)中读取缓存返回。

这里有一个巨大的漏洞:如果我本地时间不准咋办?

所以,Expires 字段几乎不被使用了。现在的项目中,我们使用 Cache-control 字段来代替 Expires 字段的强缓存功能。

Cache-control

Cache-control 是在资源的响应头上设置缓存时间,单位是秒。

response.setHeader("Cache-Control", "max-age=3600"); // 设置资源在1小时内有效

从第一次请求资源的时候开始,往后 N 秒内,资源若再次请求,则直接从内存(或磁盘)中读取,不与服务器做任何交互。

Cache-Control 有 6 个属性:

  • max-age:决定客户端资源被缓存多久。

  • s-maxag:决定代理服务器(如 nginx)缓存的时长。

  • no-cache:表示强制进行协商缓存,即跳过强缓存校验,直接去服务器进行协商缓存。

  • no-store:是表示禁止任何缓存策略。

  • public:表示资源即可以被浏览器缓存也可以被代理服务器缓存。

  • private:表示资源只能被浏览器缓存。

no-cache 和 no-store 互斥(即不能同时存在),public 和 private 互斥

Cache-Control 设置多个属性:

response.setHeader("Cache-Control", "max-age=10000,s-maxage=200000,public");

如果 Cache-Control 和 Expires 同时存在,Cache-Control 的优先级更高,会覆盖 Expires 的设置。

协商缓存

协商缓存主要有四个头字段,它们两两组合配合使用,Last-Modified 和 If-Modified-Since 一组,Etag 和 If-None-Match 一组,当同时存在的时候会以 Etag 和 If-None-Match 为主。

当命中协商缓存的时候,服务器 HTTP 状态码会返回 304,让客户端直接从本地缓存里面读取文件。

Last-Modified 和 If-Modified-Since

流程:

1、首次请求资源

1.1、浏览器发送请求:

浏览器首次向服务器请求某个资源(如 HTML、CSS、JS 文件等)

1.2、服务器响应请求:

  • 服务器在返回资源的同时,在 HTTP 响应头中添加 Last-Modified 字段,该字段的值表示资源在服务器上的最后修改时间

  • 浏览器接收资源并缓存起来,同时缓存响应头中的 Last-Modified 值

2、再次请求资源

2.1、浏览器检查缓存:

当浏览器再次请求相同的资源时,它会先检查本地缓存中是否存在该资源

2.2、构造请求头:

如果缓存资源存在且未过期且协商缓存被启用,浏览器会在 HTTP 请求头中添加 If-Modified-Since 字段,其值为上一次请求时服务器返回的 Last-Modified 值

2.3、发送请求:

浏览器将包含 If-Modified-Since 请求头的请求发送给服务器

3、服务器处理请求

3.1、检查资源修改时间:

  • 服务器收到请求后,会检查请求头中的 If-Modified-Since 字段。

  • 然后,服务器会将该字段值与资源在服务器上的当前最后修改时间做比较。

3.2、返回响应:

  • 如果资源未修改(即服务器的最后修改时间未超过 If-Modified-Since 指定的时间):

    • 服务器将返回 HTTP 状态码 304(Not Modified),并且不会返回资源内容。

    • 浏览器接收到 304 状态码后,会从本地缓存中加载资源。

  • 如果资源已修改:

    • 服务器将返回 HTTP 状态码 200(OK),并发送资源的新内容。

    • 同时,服务器还会在响应头中更新 Last-Modified 值,以便下次请求时使用。

4、浏览器更新缓存

  • 如果服务器返回了 304 状态码,浏览器将保持本地缓存不变,并继续从缓存中加载资源。

  • 如果服务器返回了 200 状态码和新的资源内容,浏览器将更新本地缓存中的资源文件和 Last-Modified 值。

这种方式的重点是判断资源文件的修改时间,以此来判断资源文件有没有被修改,而不是判断时间有效期。

这种方式的缺点是:

  1. 只要编辑了,不管内容是否真的有改变,都会更新最后修改时间。

  2. Last-Modified 过期时间只能精确到秒。如果在同一秒既修改了文件又获取文件,客户端是获取不到最新文件的。

为了解决上述问题,从 HTTP.1 开始新增了一个头信息,ETag。

ETag 和 If-None-Match

ETag 就是将 Last-Modified 那套比较时间戳的形式修改成了比较文件指纹。

文件指纹就是根据文件内容计算出的唯一哈希值。文件内容一旦改变则指纹改变。

ETag 和 If-None-Match 流程与 Last-Modified 和 If-Modified-Since 流程几乎一样,只是将 Last-Modified 替换成 ETag,If-Modified-Since 替换成 If-None-Match,比较文件修改时间替换成比较文件指纹

ETag 有强验证和弱验证:

  • 强验证:哈希码深入到每个字节,哪怕文件中只有一个字节改变了,也会生成不同的哈希值,它可以保证文件内容绝对的不变。

  • 弱验证:提取文件的部分属性来生成哈希值,整体速度会比强验证快,但是准确率不高

ETag 缺点:

  • 计算文件指纹意味着服务端需要更多的计算开销,文件尺寸大、数量多会影响服务器的性能,尤其是强验证会非常消耗计算量

很多网站在获取静态资源时会同时使用 Last-Modified 和 ETag,可能是考虑浏览器的兼容性问题吧。

区别

强制缓存协商缓存
工作原理浏览器直接从本地缓存中读取资源,不向服务器发送请求浏览器向服务器发送请求,询问资源是否更新,根据服务器响应决定是否使用缓存
HTTP头字段主要依赖 Cache-Control 或 Expires主要依赖 ETag 或 Last-Modified
响应状态码缓存命中时,无请求发出,因此无状态码缓存命中时返回 304 Not Modified
适用场景适用于不经常变动的静态资源,如图片、CSS、JavaScript文件等适用于可能被频繁更新的资源,如动态数据等
性能影响减少网络请求,提高页面加载速度,降低服务器压力仍然需要网络请求,但可以减少数据传输量,对于频繁更新的资源能确保用户获取最新内容

如果同时设置了强制缓存和协商缓存,浏览器会先判断强制缓存是否命中,如果强制缓存未命中,则再判断协商缓存是否命中。

使用 nginx 配置缓存

server {  
    listen 80;  
  
    server_name your-domain.com; # 修改为你的域名  
  
    location /vue-app/ {  
        alias /path/to/your/dist/; # 修改为你的Vue项目dist目录的实际路径  
        try_files $uri $uri/ /vue-app/index.html; # 对于单页面应用,确保所有路由都返回index.html  
  
        # 开启强制缓存   
        add_header Cache-Control "public, max-age=3000";
        # 跳过强制缓存,强制进行协商缓存
        # add_header Cache-Control "no-cache";
        # 禁用缓存
        # add_header Cache-Control "no-store";
        
        # nginx 会自动给静态文件添加 Last-Modified 和 ETag 头部,无需额外配置
        ......
    }  
  
    # 其他location块或server配置...  
}

开启 GZIP 压缩

原理

通过将静态文件(js、css、图片等)压缩为 .gz 格式的文件,减小文件体积,从而提高文件加载速度。

使用

  1. 安装插件:npm install compression-webpack-plugin

  2. 配置 webpack(此过程省略)

  3. 打包(dist 中的静态文件转为 .gz 格式)

  4. nginx 中配置 gzip_static on

节流和防抖

节流

频率极高的事件在固定时间内只触发一次。例如滚动条的滚动事件。

封装好的节流函数代码:

/*
  @params (入参)
  callback:需要节流的函数。   必传!
  time:节流间隔时间点(也就是多久触发一次)不传的话默认是 300 毫秒
*/
const onScroll = (callback, time = 300) => {
  let state = true; //触发判断条件
  //判断如否有函数传入
  if(typeof callback !== 'function'){
    throw '第一个入参必须是函数,需要被节流的函数'
  }
  //制作一个闭包环境
  return () => {
    if(state){
      callback();
      state = false;
      setTimeout(() => {
        state = true;
      }, time)
    }
  }
}

防抖

短时间内多次触发同一个事件时只执行一次。例如连续点击确定按钮只调一次方法。

封装好的防抖函数代码:

/*
  @params (入参)
  callback:需要节流的函数。   必传!
  time:防抖间隔时间点(也就是倒计时触发的缓冲时间)不传的话默认是 300 毫秒
*/
const onchange = (callback, time = 300) => {
  let asyncFun;
  //判断需要被防抖的函数是否传入
  if(typeof callback !== 'function'){
    throw '第一个入参必须是函数,需要进行防抖的函数'
  }
  //创建一个闭包环境
  return () => {
  //在上一个函数被触发前,销毁他
    if (asyncFun !== undefined) clearTimeout(asyncFun);
    //创建一个新的函数
    asyncFun = setTimeout(() => {
      callback();
    }, time)
  }
}

减少重排和重绘

重排(也叫回流)和重绘是浏览器中相对比较耗时的动作。尤其是重排。

DOM 节点元素出现删除、增加、移动、尺寸改变的情况时,浏览器会先在指定位置上构建该元素的 DOM(重排),然后再对该元素进行渲染(重绘)。

重排一定会导致重绘,重绘不一定会引起重排。

重排的触发场景

  • 删除或者新增一个节点元素

  • 元素位置的改变,比如 float、position、overflow、display 等等

  • 元素尺寸的改变,比如 margin、padding、height、width 等等

  • 初始化构建 DOM 树的时候

  • 窗口尺寸的变化,也就是 resize 事件发生的时候

  • 填充内容的改变(内容撑大了某一个节点,内容改变,包含它的节点大小自然跟随调整。)

  • ......

重绘触发场景

  • 改变 background、color、border 等

  • visibility: hidden

  • css3 的 translate

  • border-style、border-radius、background-repeat、background-size、outline-color、text-decoration、box-shadow

  • ......

图片优化

图片懒加载

核心思想在于延迟加载页面上的图片资源,直到这些资源即将出现在视口中时才开始加载。主要有两种实现方式:

1. <img loading="lazy">

HTML5 img 标签的新特性,用于开启图片延迟加载,直到图像即将进入视口才发送请求加载图像。

优点在于使用简单,但是在实际使用中往往不如预期,可控性较差,参考 图片延迟加载(懒加载)属性loading=‘lazy’实践

如果想要更加精细地控制图片懒加载,建议使用下面的方法。

2. data-src

HTML5 中我们可以使用 data-xxx 设置我们需要的自定义属性来进行一些数据的存放。前面的 data- 是固定的,后面的 xxx 一般为表示与自定义属性相关的字符串。img 标签中的 data-src 属性就属于一种自定义的 dataset 属性。

浏览器是否发起请求图片是根据 <img>的 src 属性,如果没有 src 属性,浏览器就不会发出请求去下载图片,或者把 src 属性设置成一张默认的加载效果图。所以懒加载基本的原理就是用 dataset 自定义属性取代 src 存储图片的路径,然后在检测到图片进入到可视区域的时候,再将其换为 src。

实现代码示例:

<script>
/*
window.innerHeight:获取窗口的高度 (不包括工具栏和滚动条)。
getBoundingClientRect():获取元素的左、上、右、下分别相对浏览器视窗的位置。
*/
  function imgonload() {
    let img = document.querySelectorAll("img");
    /*console.log(img);*/
    for(let i=0; i<img.length; i++) {
      // 图片距离窗口上方的位置小于窗口的高度(也就是说该图片已经进入了窗口)
      if(img[i].getBoundingClientRect().top < window.innerHeight) {
        // 赋值
        img[i].src = img[i].dataset.src;
      }
    }
  }
 
  function scollImg(fn) {
    let timer = null;
    let context = this;
    return function () {
      clearTimeout(timer);
      timer = setTimeout(() => {
        fn.apply(context);
      }, 500)
    }
  }
  window.onload = imgonload;
  // 绑定 scroll 事件,在滚动页面时触发
  window.onscroll = scollImg(imgonload);
</script>

更简单的方法是使用插件:

// 安装插件
npm install vue-lazyload --save-dev

// main.js 引用
import VueLazyload from 'vue-lazyload';
Vue.use(VueLazyload);

// 使用
<img v-lazy="img/text.png"></a>

图片转 base64

Base64图片优势在于可以用文本的形式展示图片,也就是说不需要发起 HTTP 请求去下载图片资源。

格式如下:

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO 9TXL0Y4OHwAAAABJRU5ErkJggg==">

Base64图片的缺点:

  • 文件体积会显著增加,因此不适合大型图片

  • 无法使用缓存

如何将图片转为 base64 格式:

  • 在 JavaScript 中可以使用 FileReader 方法来将图片转为 base64 格式

  • Webpack4 中配置 url-loader

  • Webpack5 中配置 asset 模块

使用 webp/svg 格式图片

  • webp

    • 优点:体积相比 png/jpg 等格式的图片会更小。

    • 缺点:兼容性较差。

  • svg

    • 优点:可缩放性和高质量,简单图形体积更小。

    • 缺点:复杂图形 svg 体积会变得相当大。

启用事件委托

前端事件知识点

路由懒加载

原本 webpack 在打包 vue 项目时会把所有路由组件打包进一个 JavaScript 文件中,可以通过使用 ES6 的动态加载模块 import() 函数,webpack 会将动态导入的路由单独打包成一个 JavaScript 文件,这样就可以实现按需加载,提高首页加载效率。

要实现路由懒加载,就得先将进行懒加载的子模块分离出来,打包成一个单独的文件,webpack 会自动帮我们做这件事。

const Home = () => import(/* webpackChunkName: "home" */ "@/views/home/index.vue");

const routes = [
    {
       path: "/",
       name: "home",
       component: Home
    },
    {
       path: "/about",
       name: "about",
       component: ()=>import("@/views/about/index.vue")
    },
    {
       path: "/user",
       name: "user",
       // webpack CommonJS 模块语法
       component: resolve=>require(["@/views/user/index.vue"] , resolve) }
    },
    ......
 ]

以上 3 种方法都可以实现路由懒加载。组件懒加载也是同理。

@ 符号的作用:

@ 符号通常被配置为一个别名(alias),用于简化模块路径的引用,是 webpack 的一个功能

// vue.config.js
chainWebpack: config => {
     // key, value自行定义
    config.resolve.alias
        .set('@', resolve('src')) // 这里的 @ 代表 src 路径
        .set('_c', resolve('src/components')) // 自定义 _c
        .set('_conf', resolve('config')) // 自定义 _conf
}

Tree Shking

作用

消除无用的 JavaScript 代码,减少代码体积。

原理

ES6 模块的设计思想是尽量静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量,这就是 tree-shaking 的基础。

所以使用 CommonJS 模块语法时就不能进行树摇,因为它是运行时加载模块的。

"ES6 模块编译时就能确定模块的依赖关系" 这句话怎么理解?

当 ES6 遇到 import 语句时,它不会像 CommonJS 那样去执行模块,而是生成一个动态的只读引用。这意味着,直到实际需要的时候,ES6 模块才会去模块内部取值,而不是像 CommonJS 那样一次性加载整个模块。这种加载方式被称为"编译时加载"或"静态加载",它使得 ES6 模块在编译时就能完成模块的加载。

注意

webpack5 和 vue-cli 中已经默认开启了 tree-shaking,所以无需再配置,但是要注意代码中的一些写法会导致树摇功能失效,例如:

// util.js
export default {
  test1(params) {
    return params;
  },
  test2(params) {
    return params;
  }
};

// 引入并使用
import util from '../util';
util.test1(null)

示例中只使用了 test1() 未使用 test2(),但是打包后 test2() 依然会存在与打包文件中,树摇失效。

优化资源加载方式

  1. script 正常模式

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

    • 有序同步下载,会阻塞 DOM 解析

    • 将 script 放在 head 里:浏览器解析 HTML,发现 script 标签时,会先下载完所有这些 script 标签中的 JavaScript 文件,再往下解析其他的 HTML,会让网页内容呈现滞后,导致用户感觉到卡。

    • 将 script 放在 body 最后:先解析完整个 HTML 页面,再下载 script 标签中的 JavaScript 文件。对于一些高度依赖于 JavaScript 的网页,就会显得慢了。

    • 将 script 放在 body 外:不合标准。但是浏览器会忽略这个错误。

    • 最优解是一边解析页面,一边下载 JavaScript 文件,所以有了下面的 2 种模式。

  2. script async 模式

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

    无序异步下载,不会阻塞 DOM 解析。

    下载完成后会立即暂停 DOM 的解析(如果此时 DOM 还未完全解析完),然后立即执行脚本,脚本执行完成后,DOM 的解析会恢复。

  3. script defer 模式

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

    有序异步下载,不会阻塞 DOM 解析。

    无论下载何时完成,都会等待整个 DOM 解析完成后才执行。

    下载的文件会在 DOMContentLoaded 事件之前执行,且按照文件出现的先后顺序执行,一般情况下都可以使用 defer。

  4. script module 模式

    <script type="module">import { a } from './a.js'</script>

  5. link preload

    <link rel="preload" as="script" href="index.js">

    • 用于提前加载一些需要的依赖,这些资源会优先加载。

    • preload 加载的资源是在浏览器渲染机制之前进行处理的,并且不会阻塞 onload 事件。

    • preload 加载的 JS 脚本其加载和执行的过程是分离的,即 preload 会预加载相应的脚本代码,待到需要时自行调用

  6. link prefetch

    <link rel="prefetch" as="script" href="index.js">

    • 该模式会在浏览器的空闲时候,加载一些未来可能会用到的资源

    • 会将资源放入缓存至少5分钟

    • 当页面跳转时,未完成的 prefetch 请求不会被中断

长列表虚拟滚动

只渲染可视区域的列表项,非可见区域不渲染,在滚动时动态更新可视区域。

插件:npm install vue-virtual-scroller

Web Worker 优化长任务

Web Worker 就是 JavaScript 中的多线程技术,允许主线程创建一个或多个 Worker 线程后台运行,等到 Worker 线程完成计算任务,再把结果返回给主线程,且这个过程中不会阻塞主线程。

Web Worker 的工作原理

  • 创建 Worker 线程:主线程通过调用 new Worker(url) 构造函数创建一个新的 Worker 线程,其中 url 是 Worker 线程将要执行的脚本文件的路径。

  • 消息传递:主线程和 Worker 线程之间通过 postMessage() 方法发送消息,并通过监听 onmessage 事件来接收消息。通信是双向的,但数据传递是拷贝的,不是共享的。

  • 终止 Worker 线程:主线程可以通过调用 Worker 对象的 terminate() [ˈtɜːmɪneɪt] 方法来终止 Worker 线程。

示例代码:

// 主线程(main.js)
if (window.Worker) {  
    // 创建一个新的 Web Worker  
    // 注意:这里假设 worker.js 位于与 HTML 文件相同的目录下 
    const myWorker = new Worker('worker.js');  
  
    // 监听来自 Worker 的消息 
    myWorker.onmessage = function(e) {  
        console.log('Received message from worker: ', e.data);  
    };  
  
    // 向 Worker 发送消息
    myWorker.postMessage('Hello, worker!');  
  
    // 当不再需要 Worker 时,可以终止它  
    // myWorker.terminate();  
}
// Worker 线程(worker.js)
self.onmessage = function(e) {  
    console.log('Received message from main script: ', e.data);  
  
    // 执行一些耗时的操作  
    const result = doSomeHeavyProcessing(e.data);  
  
    // 将结果发送回主线程  
    self.postMessage(result);  
};  
  
function doSomeHeavyProcessing(data) {  
    // 假设这里进行一些复杂的计算  
    return `Processed ${data}`;  
}

在 Web Worker 的上下文中,self.onmessage 是一个事件监听器,它用于监听来自创建它的主线程或其他 Worker 线程的消息。

self 关键字在 Web Worker 的上下文中是一个指向全局 Worker 对象的引用,与在浏览器主线程中使用的 window 对象类似。但是,在 Worker 中,window 对象是不可用的,因此使用 self 来代替。

self.onmessage = function(e) {
    ......
}

// 简化写法,效果相同
onmessage = function(e) {  
  ......
};

Web Worker 中只能获取到部分浏览器提供的 API,如定时器、navigator、location、XMLHttpRequest(意味着可以使用 Ajax 请求) 等。

但是并不是所有的任务都适合开启 Web Worker,因为新建一个 Web Worker 时浏览器会加载对应的 worker.js 资源,这个过程会消耗时间,所以只有当任务的运算时间大于消耗时间才适合使用 Web Worker。

当面试官问你当页面处理10W条数据如何保证浏览器不卡顿时,你可以回答:使用 Web Worker。

骨架屏优化白屏

SPA 单页应用,无论 vue 还是 react,最初的 html 都是空白的,使用骨架屏,可以缩短白屏时间,提升用户体验。国内大多数的主流网站都使用了骨架屏,特别是手机端的项目。

骨架屏插件:npm i vue-skeleton-webpack-plugin

参考资料

前端性能优化——首页资源压缩63%、白屏时间缩短86%

中高级前端工程师必备14种性能优化方案

中高级前端工程师都需要熟悉的技能--前端缓存 - 掘金 (juejin.cn)

一文!彻底弄懂前端缓存_zz_jesse的博客-CSDN博客