您现在的位置是:首页 >技术杂谈 >前端系列之:性能优化网站首页技术杂谈

前端系列之:性能优化

程序员SKY 2026-07-02 00:01:05
简介前端系列之:性能优化

启用前端缓存

浏览器缓存

Cookie

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

  • 只能存字符串类型数据

  • Cookie 的属性

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

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

    • domain:Cookie 的作用域名。

    • HttpOnly:布尔值,如果设置 true,浏览器将禁止通过 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());
  
        // 其他逻辑代码...
  
        // 响应完成
    }
}

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 样式表、API 数据等)时,会发起一个普通的 HTTP 请求。

1.2、服务器响应请求:

服务器接收到请求后,会返回对应的资源内容。同时,在 HTTP 响应头中添加 Last-Modified 字段,该字段的值是资源在服务器上的最后修改时间。

浏览器接收到响应后,会将资源缓存到本地,并且缓存响应头中的 Last-Modified 值。

示例响应头:

HTTP/1.1 200 OK
Content-Type: text/html
Last-Modified: Tue, 12 Feb 2024 10:30:00 GMT
...

2、再次请求资源

2.1、浏览器检查缓存:

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

2.2、构造请求头:

若本地缓存中存在该资源,且缓存未被禁用,同时协商缓存机制处于启用状态,浏览器会在新的 HTTP 请求头中添加 If-Modified-Since 字段,其值为上一次请求时从服务器获取的 Last-Modified 值。

示例请求头:

GET /example.html HTTP/1.1
Host: example.com
If-Modified-Since: Tue, 12 Feb 2024 10:30:00 GMT
...

2.3、发送请求:

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

3、服务器处理请求

3.1、检查资源修改时间:

服务器收到请求后,会检查请求头中的 If-Modified-Since 字段,并将该字段的值与服务器上该资源的当前最后修改时间进行比较。

3.2、返回响应:

  • 如果资源未修改:

    即服务器上资源的最后修改时间没有晚于 If-Modified-Since 指定的时间。

    此时,服务器会返回 HTTP 状态码 304(Not Modified),并且不会返回资源的具体内容。

    示例响应头:

    HTTP/1.1 304 Not Modified
    ...
    
  • 如果资源已修改:

    即服务器上资源的最后修改时间晚于 If-Modified-Since 指定的时间。

    服务器会返回 HTTP 状态码 200(OK),并发送更新后的资源内容。同时,服务器会在响应头中更新 Last-Modified 字段的值,以便下次请求时使用。

    示例响应头:

    HTTP/1.1 200 OK
    Content-Type: text/html
    Last-Modified: Tue, 12 Feb 2024 11:00:00 GMT
    ...
    

4、浏览器更新缓存

  • 304 状态码:当浏览器接收到 304 状态码时,意味着资源未修改,浏览器会保持本地缓存不变,并直接从本地缓存中加载资源。

  • 200 状态码:若浏览器收到 200 状态码和新的资源内容,说明资源已更新,浏览器会更新本地缓存中的资源文件以及对应的 Last-Modified 值。

机制缺点:

  1. 编辑即更新时间

    只要对资源文件进行了编辑操作,无论文件内容是否真的发生改变,服务器都会更新该资源的最后修改时间。这可能会导致不必要的资源重新请求,影响性能。

  2. 时间精度问题

    Last-Modified 时间只能精确到秒。如果在同一秒内既对文件进行了修改又发起了获取文件的请求,客户端可能无法获取到最新的文件,因为时间比较上会认为文件未修改。

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

ETag 和 If-None-Match

ETag 和 If-None-Match 流程与 Last-Modified 和 If-Modified-Since 流程几乎一样,只是将之前比较时间戳的形式变成了比较文件指纹。

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

ETag 有强验证和弱验证:

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

  • 弱验证:提取文件的部分属性来生成哈希值,整体速度会比强验证快,但是准确性,可能会出现文件内容实际上有变化,但指纹未改变的情况。

ETag 缺点:计算文件指纹会使服务器产生额外的计算开销。当文件尺寸较大、数量较多时,会对服务器性能造成影响。尤其是强验证方式,由于需要对文件的每个字节进行处理,计算量巨大,会显著增加服务器的负担。

在实际应用中经常会同时使用 Last-Modified 和 ETag,一般情况下,服务器会优先根据 ETag 判断,因为它能提供更精确的匹配。如果 ETag 不存在或无法使用,服务器会回退到 Last-Modified,因为 Last-Modified 兼容性更好。

区别

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

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

使用 nginx 配置缓存

// 后端 java 服务器设置缓存的方式:
response.setHeader("Cache-Control", "max-age=3600");
......
# 前端 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 会把所有的路由组件打包进一个 JavaScript 文件。如果不做优化,内容过大的文件可能会导致首页加载变慢。

通过使用 ES6 的动态导入(import()),可以实现按需加载路由组件,webpack 会自动将这些动态导入的路由组件打包成单独的文件。这就意味着只有在用户访问特定路由时,相应的 JavaScript 文件才会被加载,从而减少初始加载的文件大小,提高首页的加载效率。

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

const routes = [
    {
       path: "/",
       name: "home",
       // 动态导入方式 1
       component: Home
    },
    {
       path: "/about",
       name: "about",
       // 动态导入方式 2
       component: ()=>import("@/views/about/index.vue")
    },
    {
       path: "/user",
       name: "user",
       // 动态导入方式 3,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() 依然会存在打包文件中,树摇失效。

这是因为 export default 导出了整个对象,而这个对象包含了 test1 和 test2 方法。

正确写法:

// util.js
export function test1(params) {
  return params;
}

export function test2(params) {
  return params;
}

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

优化资源加载方式

script 优化

  1. script 正常模式

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

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

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

    • 将 script 放在 body 开头:同上。

    • 将 script 放在 body 最后:先解析完整个 HTML 页面,再下载 script 标签中的 JavaScript 文件。最常见,因为 JavaScript 文件经常会操作 DOM,需要确保 DOM 元素已经完全加载。

    • 将 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。

    • 适用于需要等待文档加载完成后再执行的脚本,尤其是当多个 <script> 标签之间有依赖关系时。

总结:

  • 正常模式的 script 标签建议放在 body 结尾处;

  • async/defer 模式的 script 标签建议放在 head 中。

link 优化

linkpreloadprefetch
示例<link rel="preload" as="script" href="index.js"><link rel="prefetch" as="script" href="index.js">
目的用于提前加载当前页面即将使用的重要资源(如字体、关键 CSS、JS 等)用于提前加载未来可能需要的资源(如下一个页面的资源)
加载优先级浏览器会以高优先级加载这些资源浏览器会以低优先级加载这些资源,只有在空闲时才会加载
适用场景适用于当前页面中很快就会用到的资源适用于预加载用户可能访问的下一个页面的资源
是否会阻塞 DOM 渲染本身不会阻塞 DOM 渲染,但如果加载的资源是关键资源(如关键 CSS 或 JS),可能会间接影响渲染不会阻塞 DOM 渲染,因为它的优先级较低,只在浏览器空闲时加载

这里的 as 属性告诉浏览器资源的类型,可以是 style、script 等

当页面跳转时,未完成的 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 是一个事件监听器,用于监听来自主线程的消息。

在 Web Worker 里,self 代表 Worker 线程自身的全局对象。它类似于在主线程中使用的 window 对象,window 是主线程的全局对象,而 self 则是 Worker 线程的全局对象。借助 self,你能够访问 Worker 线程环境下的全局属性和方法。

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

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

// 与 window 可以省略相同,self 也可以省略
onmessage = function(e) {  
  ......
};

但是并不是所有的任务都适合开启 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博客

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。