前端性能优化_白屏/首屏加载

1 路由懒加载

SPA单页面应用项目,一个路由对应一个页面,如果不做处理,项目打包后,会把所有页面打包成一个文件,当用户打开首页时,会一次性加载所有资源,造成首页加载很慢,降低用户体验。(前后大概降低30%)

// 请只在生产时懒加载,否则路由多起来后,开发时的构建速度感人

// 同步加载的路由
{
  name: 'personHomeHome',
  path: '/personHome/home',
  meta: {
    title: '个人首页',
    breadcrumb: false,
    dot: false,
    icon: 'iconfont iconzhuye1',
  },
  component: (resolve) => require(['@/views/personHome/home/home.vue'], resolve),
},
// 异步加载
{
  path: 'editor',
  meta: { 
    requireLogin: true,
    istrun:true 
  },
  component: () => import(/* webpackChunkName: 'root' */ '@/views/form/editor')
},

2 组件懒加载

除了路由的懒加载外,组件的懒加载在很多场景下也有重要的作用

例如:当用户打开某个页面,会一次性加载该页面所有的资源,我们期望的是有些组件使用户触发按钮后,再加载该弹窗组件的资源,例如弹窗组件,这时候,就可以考虑用懒加载的方式引入

// 没有懒加载时候
import treeTable from "@/components/table/treeTable.vue";
import editDialog from "./editDialog.vue";
export default {
  components: {
    treeTable,
    editDialog,
  },
}
// 改成懒加载
const editDialog = ()=> import(/*webpackChunkName: "editDialog"*/ "./editDialog.vue")
const treeTable = ()=> import(/*webpackChunkName: "treeTable"*/ "@/components/table/treeTable.vue");
export default {
  components: {
    treeTable,
    editDialog,
  },
}

此时打包之后,弹窗组件就被单独打包出来editDialog对应的js 和css ,当用户点击按钮时,才会去加载。

组件懒加载的场景:

  • 该页面的js文件体积大,导致页面打开很慢,可以通过组件懒加载进行资源拆分,利用浏览器并行下载资源,提升下载速度。
  • 该组件不是已进入页面就展示,需要一定条件下才触发(例如弹窗组件)
  • 该组件复用性高,很多页面都有引入,利用组件懒加载抽离出该组件,一方面可以很好利用缓存,同时也可以减少页面的 js 文件大小(比如表格组件、图形组件)

3 优化分包策略

vue-cli3 的默认优化是将所有npm依赖都打进 chunk-vendor,但这种做法在依赖多的情况下导致 chunk-vendor 过大

splitChunks 优化分包

 configureWebpack: (config) => {
    if (process.env.NODE_ENV === 'production') {
    config.optimization.splitChunks({
              chunks: 'all',
              cacheGroups: {
                libs: { // libs 缓存组
                  name: 'chunk-libs', // 该缓存组的名称 chunk-libs
                  test: /[\/]node_modules[\/]/, // [\/] 匹配 / 或 \,匹配路径中包含node_modules的模块
                  priority: 10, // 缓存组的优先级,数字越小,优先级越高
                  chunks: 'initial' //初始的依赖模块 only package third parties that are initially dependent
                },
                elementUI: {
                  name: 'chunk-elementUI', // split elementUI into a single package
                  priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
                  test: /[\/]node_modules[\/]_?element-ui(.*)/ // in order to adapt to cnpm
                },
                commons: {
                  name: 'chunk-commons',
                  test: resolve('src/components'), // can customize your rules
                  minChunks: 3, //  minimum common number
                  priority: 5,
                  reuseExistingChunk: true
                }
              }
            })
        }
        // https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk
        config.optimization.runtimeChunk('single') // 为每个入口点生成一个运行时代码块
      }

externals 配合CDN 加速

用 webpack 的externals 属性把把不需要打包的库文件分离出去,减少打包后的文件的大小,用cdn 资源的方式将第三方依赖包引入项目,可以大大减少项目打包体积

// 库文件内容准备
vue@2.7.14

// 配置 externals 对象的技巧
key: 就是 import AAA from 'BBB'中的 BBB
value: 就是可以在引入CDN 资源后,在控制台打印 window.XXX, value 就是 XXX

@1 CDN 引入vue
如果npm 已安装了vue ,可以选择卸载,不卸载也行,但是 vue-template-compiler 需要安装,注意版本号和vue 版本保持一致
yarn add vue-template-compiler@2.7.14 -S
// vue@2.7.14      // Vue 的版本号
public文件夹里的index.html 文件引入vue
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
@2 webpack.config.js中的配置
// 在这里配置后,减少了压缩的包内容,需要在public/index.html通过cdn方式再引入,注意对应的版本
chainWebpack (config) {
    config.externals = { 
      vue: 'Vue',
    },
}
// 然后项目里所有的import Vue from 'vue' 都可以去掉了,注意Vue是大写;
原理是通过 cdn 引入的vue 会挂载到 window 下面, window.Vue

4 合理使用 Tree shaking

Tree shaking的作用:消除无用的js 代码,减少代码体积

例如:

export function isFunction(obj) {
        return typeof obj === "function" &&
            typeof obj.nodeType !== "number" &&
            typeof obj.item !== "function";
    };
export function isPlainObject(obj) {
        let proto, Ctor;
        if (!obj || toString.call(obj) !== "[object Object]") return false;
        proto = Object.getPrototypeOf(obj);
        if (!proto) return true; //匹配 Object.create(null)
        Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
        return typeof Ctor === "function" && Ctor === Object;
    };

项目中只使用 isFunction 没有使用 isPlainObject 方法,项目打包后,isPlainObject 方法不会被打包到项目里

Tree shaking原理:

依赖于 ES6的模块特性, ES6 模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析这是 Tree shaking的基础

静态分析就是不需要执行代码,就可以从字面量上对代码进行分析。 ES6 之前的模块,比如 CommonJs 是动态加载,只有执行后才可以引用的什么模块,就不能通过静态分析去做优化,正是基于这个基础上,才使得 Tree shaking 成为可能

Tree shaking并不是万能的

并不是所有无用的代码都可以被消除,还是上面的代码,换个写法 Tree shaking 就失效了

export default { 
    isFunction(obj) {
        return typeof obj === "function" &&
            typeof obj.nodeType !== "number" &&
            typeof obj.item !== "function";
    },
    isPlainObject(obj) {
        let proto, Ctor;
        if (!obj || toString.call(obj) !== "[object Object]") return false;
        proto = Object.getPrototypeOf(obj);
        if (!proto) return true; //匹配 Object.create(null)
        Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
        return typeof Ctor === "function" && Ctor === Object;
    },
  }

同样的,项目中只使用 isFunction 没有使用 isPlainObject 方法,项目打包后,isPlainObject 方法还是被打包到项目里,

原因是 export default 导出的是一个对象,无法通过静态分析判断出一个对象的哪些变量未被使用,所以 tree shaking 只对使用 export 单个导出的变量生肖

这也是函数式编程越来越被青睐的原因,因为可以很好利用 tree-shaking 精简项目的体积

5 骨架屏优化白屏时长

SPA单页面应用,无论是 vue 还是 react ,最初的 html 都是空白的,需要通过加载 js 将内容挂载到跟节点上,这套机制的副作用:会造成长时间的白屏

常见的骨架屏插件就是基于这种原理,在项目打包时,将骨架屏的内容直接放到 html 文件的根节点中

骨架屏插件 vue-skeleton-webpack-plugin 插件为例,该插件的亮点时可以给不同的页面设置不同的骨架屏

// 安装
yarn add vue-skeleton-webpack-plugin
// 使用 
// vue.config.js 配置
// 骨架屏
const SkeletonWebpackPlugin = require("vue-skeleton-webpack-plugin");
module.exports = {
   configureWebpack: {
      plugins: [
       new SkeletonWebpackPlugin({
        // 实例化插件对象
        webpackConfig: {
          entry: {
            app: path.join(__dirname, './src/skeleton.js') // 引入骨架屏入口文件
          }
        },
        minimize: true, // SPA 下是否需要压缩注入 HTML 的 JS 代码
        quiet: true, // 在服务端渲染时是否需要输出信息到控制台
        router: {
          mode: 'hash', // 路由模式
          routes: [
            // 不同页面可以配置不同骨架屏
            // 对应路径所需要的骨架屏组件id,id的定义在入口文件内
            { path: /^\/home(?:\/)?/i, skeletonId: 'homeSkeleton' },
            { path: /^\/detail(?:\/)?/i, skeletonId: 'detailSkeleton' }
          ]
        }
      })        
      ]
   }
}
// 新建 skeleton.js 入口文件 
// skeleton.js
import Vue from "vue";
// 引入对应的骨架屏页面
import homeSkeleton from "./views/homeSkeleton";
import detailSkeleton from "./views/detailSkeleton";

export default new Vue({
    components: {
        homeSkeleton,
        detailSkeleton,
    },
    template: `
    <div>
      <homeSkeleton id="homeSkeleton" style="display:none;" />
      <detailSkeleton id="detailSkeleton" style="display:none;" />
    </div>
  `,
});

6 长列表虚拟滚动

首页中不乏有需要渲染长列表的场景,当渲染条数过多时,所需要的渲染时间会很长,滚动时还会造成页面卡顿,整体体验非常不好

虚拟滚动——指的是只渲染可视区域的列表项,非可见区域的不渲染,在滚动时动态更新可视区域,该方案在优化大量数据渲染时效果是很明显的

前端性能优化_白屏/首屏加载

虚拟滚动原理: 计算出 totalHeight 列表总高度,并在触发滚动事件时根据 scrollTop 指不断更新 startIndex 以及 endIndex ,以此从列表数据 listData 中截取对应元素

虚拟滚动插件vue 和react 中都有很多 vue-virtual-scroller、vue-virtual-scroll-list、react 中有 react-tiny-virtual-list、react-virtualized

这里简单介绍 vue-virtual-scroller 的使用

// 1. 安装插件
yarn add vue-virtual-scroller
// 2. 使用
// main.js 文件中
import VueVirtualScroller from 'vue-virtual-scroller' // 引入文件
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' // 引入样式

// 全局注册
Vue.use(VueVirtualScroller)

// 使用指定组件
import Vue from 'vue'
import { RecycleScroller } from 'vue-virtual-scroller'
Vue.component('RecycleScroller', RecycleScroller)

// 页面使用
<template> 
  <RecycleScroller 
    class="scroller" 
    :items="list" 
    :item-size="32" 
    key-field="id" 
    v-slot="{ item }"> 
      <div class="user"> {{ item.name }} </div>
  </RecycleScroller> 
</template>
// RecycleScroller可参考:https://github.com/Akryum/vue-virtual-scroller/tree/v1/packages/vue-virtual-scroller

该插件主要有 RecycleScroller 和 DynamicScroller 这两个组件,其中 RecycleScroller 需要设置 item 高度,也就是列表每项 item 的高度都是一致的, 而 DynamicScroller 可以兼容 item 的高度为动态的

7 Web Worker 优化长任务

长任务: 执行时间超过 50ms 的任务

由于浏览器 GUI 渲染线程与 js 引擎线程是互斥的关系,当页面中有很多长任务时,会造成页面 UI 阻塞。出现界面卡顿情况

查看页面的长任务:

打开控制台,选择 Performance 工具,点击 start 按钮,展开 Main 选项,会发现有很多红色的三角,这些就属于长任务

前端性能优化_白屏/首屏加载

Web Worker的通信时长

并不是执行时间超过 50ms 的任务,就可以使用 Web Worker, 还需要考虑通信时长的问题,假如一个运算执行时长为100ms ,但是通信时长为300ms ,用了 Web Worker 可能会更慢。

所以当任务的运算时长 减去 通信时长 大于 50ms ,可以考虑使用 Web Worker

8 requestAnimationFrame 制作动画

requestAnimationFrame 是浏览器专门为动画提供的 API , 它的刷新频率与显示器的频率保持一致,使用该 API 以解决用 setTimeout/setInterval 制作动画卡顿的情况

requestAnimationFrame 和 setTimeout /setInterval 的区别

引擎层面

setTimeout /setInterval 属于 js 引擎, requestAnimationFrame 属于 GUI 引擎。
js 引擎与GUI 引擎 是互斥的,也就是说 GUI 引擎在渲染时会阻塞JS 引擎的计算

时间准确性

requestAnimationFrame 刷新频率是固定且准确的,但 setTimeout /setInterval 是宏任务,根据时间轮询机制,其他任务会阻塞或延迟 js 任务的执行,会出现定时器不准的情况

性能方面

当页面被隐藏或最小化时,setTimeout /setInterval 定时器仍会在后台执行动画任务,而使用 requestAnimationFrame 当页面处于未激活的状态下,屏幕刷新任务会被系统暂停

9 优化js加载方式

js一般加载模式

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

这种情况下 js 会阻塞 dom 渲染,浏览器必须等待 index.js 加载和执行完成后才能去做其他事情

async 模式

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

async 模式下,他是异步加载的,js 不会阻塞 DOM 的渲染, async 加载是无顺序的,当他加载结束,js 会立即执行

defer 模式

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

defer 模式下, js 的加载也是异步的,defer 资源会在 DOMContentLoaded 执行之前且defer有顺序的加载

如果有多个设置了 defer 的 script 标签,则会按照引入的前后顺序执行,即便是后面的script 资源先返回

module 模式

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

在当代主流浏览器中, script 标签的属性可以加上 type=”module”,浏览器会对其内部的 import 引用发起 HTTP 请求,或者模块内容。这时 script 的行为会像是 defer 一样,在后台下载并且等待 DOM 解析

preload

link 标签的 preload 属性:用于提前加载一些需要的依赖,这些资源会优先加载。

<link rel="preload" as="script" href="index.js">
// 配置文件中 vue.config.js
// it can improve the speed of the first screen, it is recommended to turn on preload
    config.plugin('preload').tap(() => [
      {
        rel: 'preload',
        // to ignore runtime.js
        // https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171
        fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/],
        include: 'initial'
      }
    ])

vue2 项目打包生成的 index.html 文件,会自动给首页所有需要的资源,全部添加 preload ,实现关键资源的提前加载

前端性能优化_白屏/首屏加载

preload 特点

  • preload加载的资源是在浏览器渲染机制之前进行处理的,并且不会阻塞 onload 事件
  • preload加载的js 脚本其加载和执行的过程是分离的 既 preload 会预加载相应的脚本代码,待到需要时自行调用

prefetch

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

prefetch 是利用浏览器的空闲时间,加载页面将来可能用到的资源的一种机制;通常可以用于加载其他页面(非首页)所需要的资源,以便加快后续页面的打开速度。

prefetch特点

  • prefetch 加载的资源可以获取非当前页面所需要的资源,并且将其放入缓存至少5分钟(无论资源是否可以缓存)
  • 当页面跳转时,未完成的 prefetch 请求不会被中断

10 图片优化

图片的动态裁剪

很多云服务,比如阿里云或七牛云阿里云七牛云 都提供了图片的动态裁剪功能,效果很好,缺点需要花钱

只需在图片的url地址上动态添加参数,就可以得到你所需要的尺寸大小,比如:http://7xkv1q.com1.z0.glb.clouddn.com/grape.jpg?imageView2/1/w/100/h/100

图片懒加载

由于浏览器会自动对页面中的 img 标签的 src 属性发送请求并下载图片,可以通过 HTML5 自定义属性 data-xxx 先暂存 src 的值,然后再图片出现在屏幕可视窗口的时候,再将data-xxx的值重新赋值到 img 的src 属性即可

// 1.安装 
yarn add vue-lazyload --save
// main.js 文件
import VueLazyload from 'vue-lazyload'
// Vue.use(VueLazyload) //无配置项
// 配置项
const loadimage = require('assets/img/common/loading.gif')
// const errorimage = require('assets/img/common/error.gif')
Vue.use(VueLazyload, {
  preLoad: 1.3, //预加载的宽高比
  loading: loadimage, //图片加载状态下显示的图片
  // error: errorimage, //图片加载失败时显示的图片
  attempt: 1, // 加载错误后最大尝试次数
})


// 使用 注意要加绑定key 【:key】
<ul>  
    <li v-for="img in list">
        <img v-lazy="img.src" :key="img.src" >
    </li>
</ul>

字体图标

图片转 base64

参考博客
blog.csdn.net/muzidigbig/…
juejin.cn/post/718889…

骨架屏文章
blog.csdn.net/tangxiujian…

原文链接:https://juejin.cn/post/7325622273107394579 作者:zcm花开不败

(0)
上一篇 2024年1月19日 下午4:21
下一篇 2024年1月19日 下午4:32

相关推荐

发表回复

登录后才能评论