Vue首屏优化方案

首屏速度是用户体验的最关键一环,而首屏速度最大的决定性因素是资源的加载速度。

而资源加载的速度=资源大小(重点)+网速

在Vue项目中,引入到工程中的所有js、css等文件,编译时都会被打包进vendor.js,浏览器在加载该文件之后才能开始显示首屏。若是引入的库众多,那么vendor.js文件体积将会相当的大,影响首屏的体验。可以看个例子:

这是优化前的页面加载状态:执行 npm run build 打包项目,出来的vendeor.js文件,基本都是1M以上的的巨大文件,没有用户能忍受5s以上的loading而不关闭页面的,如图所示:

测量vue首屏加载速度 - 使用开发者工具

Chrome开发者工具
  1. 打开开发者工具:在Chrome浏览器中,按F12或右键点击页面选择“检查”,打开开发者工具。

  2. 记录首屏加载时间

    刷新页面。
    在Network标签页中,可以看到加载资源的瀑布图。
    观察DOMContentLoaded事件和load事件的时间。DOMContentLoaded事件通常标志着DOM的构建完成,而load事件表示所有资源(包括图片、样式表等)都已加载完成。

当项目在挂载到服务器上,平均都是5s+以上加载出来,好家伙这加载时间,仿佛过了半个世纪,很烦人,心态boom, 开发者甚至都有种想砸电脑的冲动  

Vue中计算白屏时间、首屏加载时间 

方式一:使用performance.timing 

// 白屏时间
// Vite环境下Vue项目的main.js或入口文件中引入这段代码
if ("performance" in window) {
  const perf = performance;
  let fpTime;
 
  if (perf.getEntriesByName) {
    const entry = perf.getEntriesByName("first-paint")[0];
    if (entry) {
      fpTime = entry.startTime;
    }
  } else if (window.performance.timing) {
    // 对不支持Performance Paint Timing API的老版本浏览器,可以尝试使用navigationStart和firstPaint
    fpTime = perf.timing.firstPaintTime;
    // 若老版浏览器也不支持,则可考虑自定义测量方式,如设置pageStartTime
  }
 
  if (fpTime) {
    // fpTime即为白屏时间,需要转换为相对时间戳(相对于navigationStart)
    const whiteScreenTime = fpTime - perf.timing.navigationStart;
    console.log("白屏时间: ", whiteScreenTime.toFixed(2), "ms");
  }
}
// 测量首屏加载时间
if ('performance' in window) {
  const t = performance.timing;
  const loadTime = t.loadEventEnd - t.navigationStart;
  console.log(`首屏加载时间: ${loadTime} ms`);
}
// 首屏加载时间
// 计算First Contentful Paint (FCP)
let fcpTime;
 
if (perf.getEntriesByName) {
  const entry = perf.getEntriesByName("first-contentful-paint")[0];
  if (entry) {
    fcpTime = entry.startTime;
  }
}
 
if (fcpTime) {
  const firstScreenTime = fcpTime - perf.timing.navigationStart;
  console.log("首屏时间: ", firstScreenTime.toFixed(2), "ms");
}

方式二:使用Lighthouse

Lighthouse是一个开源的自动化工具,用于改善网页的质量。它可以帮助你发现性能瓶颈、可访问性问题和SEO问题。

  1. 访问Lighthouse:在Chrome开发者工具中,点击“Lighthouse”标签。

  2. 运行测试:选择你想要测试的URL,点击“生成报告”。

  3. 查看结果:在报告中,你会看到关于性能、可访问性、最佳实践和SEO的评分和建议。

方式三:使用Web Vitals库

Web Vitals是一套核心的网站性能指标,包括LCP(最大内容绘制时间)、FID(首次输入延迟)和CLS(累积布局偏移)。这些指标对于用户体验至关重要。你可以使用Chrome的用户体验报告来监控这些指标。 

有一些Vue插件或库可以帮助你更简单地获取和监控页面的性能数据,例如vue-performance。这些插件通常提供了更直观的API来获取和展示性能数据。

npm install web-vitals --save-dev
import { getCLS, getFID, getLCP, getTTFB } from 'web-vitals';
 
function sendToAnalytics(metric) {
  // 发送到分析工具,如Google Analytics等
  console.log(metric); // 临时打印或发送到你的分析系统
}
 
getCLS(sendToAnalytics); // 累积布局偏移量(Cumulative Layout Shift)
getFID(sendToAnalytics); // 首次输入延迟(First Input Delay)
getLCP(sendToAnalytics); // 最大内容绘制(Largest Contentful Paint)
getTTFB(sendToAnalytics); // 首次字节到首次绘制(Time to First Byte)

一、分析下前端加载速度慢原因

第一步:首先安装webpack的可视化资源分析工具,命令行执行:

 npm i webpack-bundle-analyzer -D

第二步:然后在webpack的dev开发模式配置中,引入插件,代码如下:

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
 
plugins: [
    new BundleAnalyzerPlugin()
]

第三步:最后命令行执行 npm run build --report  , 浏览器会自动打开分析结果,如下所示:

 

 可以看到vue全家桶相关依赖占用了很大的空间,对webpack的构建速度和网站加载速度都会有比较大的影响。单页应用会随着项目越大,导致首屏加载速度很慢,针对目前所暴露出来的问题,有以下几种优化方案可以参考: 

二、优化方案

优化分类: 

  1. 网络优化
  2. 代码优化
  3. webpack、压缩体积优化
  4. 资源优化

常见的几种SPA首屏优化方式

  • 减小入口文件积;UI框架按需加载

  • 代码层面的优化 - 代码尽量精简

  • 减少dom操作,减少重排

  • 移除不再需要的事件监听,防止内存泄漏

  • 网络请求优化

  • 开启Brotli,GZip压缩

  • 图片资源的压缩

  • 采用第三方CDN资源

  • 资源加载优化

  • 利用浏览器缓存;静态资源本地缓存

0.初步优化

初步优化:减少全局组件、插件的引入(是否放在main.js),按需引入需要的模块(echarts按需引入等),使用轻量级数据库(moment.js 切换 data-fns等) 

使用时引入插件:在组件mounted阶段再引入库,或者用到这个功能时再引入

一些非马上用的操作改成异步:

1.采用异步组件和懒加载的方式

异步组件

这样做的目的是在首屏渲染时减少加载文件的数量,在需要用到一些组件时才会从服务器获取,而且不会重复请求,可以有效的减少白屏的时间。

懒加载第三方库(例如使用webpack的动态导入)
// 在需要的时候才加载库
import('some-library').then(library => {
  // 使用library
});
路由懒加载
{
    path: '/Login',
    name: 'Login',
    component: () => import('@/view/Login')
}
组件重复打包

假设A.js文件是一个常用的库,现在有多个路由使用了A.js文件,这就造成了重复下载

解决方案:在webpackconfig文件中,修改CommonsChunkPlugin的配置

minChunks:3,

minChunks为3表示会把使用3次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件

路由预加载

SPA 项目,一个路由对应一个页面,如果不做处理,项目打包后,会把所有页面打包成一个文件,「当用户打开首页时,会一次性加载所有的资源」,造成首页加载很慢,降低用户体验 

访问到当前页面才会加载相关的资源,异步方式分模块加载文件,默认的文件名是随机的id。如果在output中配置了chunkFilename,可以在component中添加WebpackChunkName,是为了方便调试,在页面加载时候,会显示加载的对应 文件名.hash 值,如下图:

{
    path: '/Login',
    name: 'Login',
    component: () = >import( /* webpackChunkName: "Login" */  '@/view/Login')
}

路由懒加载的原理:

懒加载前提的实现:ES6 的动态地加载模块——import()

图片懒加载:使用vue-lazyload插件

Vue 图片懒加载 之 Vue-Lazyload-优快云博客

//引入vue懒加载
import VueLazyload from 'vue-lazyload'
 
//方法一:  没有页面加载中的图片和页面图片加载错误的图片显示
// Vue.use(VueLazyload)
 
//方法二:  显示页面图片加载中的图片和页面图片加载错误的图片
//引入图片
import loading from '@/assets/images/load.jpg'
//注册图片懒加载  
Vue.use(VueLazyload, {
  // preLoad: 1.3,
  error: '@/assets/images/error.jpg',//图片错误的替换图片路径(可以使用变量存储)
  loading: loading,//正在加载的图片路径(可以使用变量存储)
  // attempt: 1
})

使用:

  <div class="lazyLoad">
    <ul>
      <li v-for="img in arr" :key="img.id">
        <img v-lazy="img.thumbnail_pic_s">
      </li>
    </ul>
  </div>

合理地分割代码模块,不仅可以使代码结构更加清晰,还能够提高加载效率。只加载用户当前需要的部分,避免一次性加载整个应用的所有代码。

2.webpack开启gzip压缩文件传输模式

Gzip 是一种用于文件压缩与解压缩的文件格式。它基于 Deflate 算法,可将文件(译者注:快速地、流式地)压缩地更小,从而实现更快的网络传输。Web 服务器与现代浏览器普遍地支持 Gzip,这意味着服务器可以在发送文件之前自动使用 Gzip 压缩文件,而浏览器可以在接收文件时自行解压缩文件。

gizp压缩是一种http请求优化方式,通过减少文件体积来提高加载速度。html、js、css文件甚至json数据都可以用它压缩,可以减小60%以上的体积。

前端配置gzip压缩,并且服务端使用nginx开启gzip,用来减小网络传输的流量大小。

 webpack打包时借助 compression webpack plugin实现gzip压缩,安装插件如下:

npm i compression-webpack-plugin

npm install compression-webpack-plugin@6.1.1 --save-dev

在vue-cli 3.0 中,vue.config.js配置如下:

const CompressionPlugin = require('compression-webpack-plugin');//引入gzip压缩插件
module.exports = {
    plugins:[
        new CompressionPlugin({//gzip压缩配置
            filename: '[path][base].gz',
            algorithm: 'gzip',  // 压缩算法,官方默认压缩算法是gzip
            test:/\.js$|\.css$|\.html$|\.eot$|\.woff$/,// 使用gzip压缩的文件类型
            threshold:10240,//对超过10kb的数据进行压缩,默认是10240
            deleteOriginalAssets:false,//是否删除原文件
            minRatio: 0.8,  // 最小压缩比率,默认是0.8
        })
    ]
}

启用gzip压缩打包之后,会变成下面这样,自动生成gz包。目前大部分主流浏览器客户端都是支持gzip的,就算小部分非主流浏览器不支持也不用担心,不支持gzip格式文件的会默认访问源文件的,所以不要配置清除源文件。

在nginx中开启gzip:

server{
    //开启和关闭gzip模式
    gzip on;
    //gizp压缩起点,文件大于2k才进行压缩;设置允许压缩的页面最小字节数,页面字节数从header头得content-length中进行获取。 默认值是0,不管页面多大都压缩。建议设置成大于2k的字节数,小于2k可能会越压越大。
    gzip_min_length 2k;
    // 设置压缩所需要的缓冲区大小,以4k为单位,如果文件为7k则申请2*4k的缓冲区 
    gzip_buffers 4 16k;
    // 设置gzip压缩针对的HTTP协议版本
    gzip_http_version 1.0;
    // gzip 压缩级别,1-9,数字越大压缩的越好,也越占用CPU时间
    gzip_comp_level 2;
    //进行压缩的文件类型
    gzip_types text/plain application/javascript text/css application/xml;
    // 是否在http header中添加Vary: Accept-Encoding,建议开启
    gzip_vary on;
}

配置好之后,打开浏览器访问线上,F12查看控制台,如果该文件资源的响应头里显示有Content-Encoding: gzip,表示浏览器支持并且启用了Gzip压缩的资源

3.Webpack 代码分割 

模块拆分: 配置Webpack将代码拆分成多个小块,利用Tree Shaking、代码压缩等技术减少代码体积。这将减少初始加载所需的下载时间,提高页面加载速度。  

// vue.config.js
module.exports = {
  configureWebpack: {
    optimization: {
      splitChunks: {
        chunks: 'all',
        minSize: 30000,
      }
    }
  }
};
---改变webpack的输出结果

       其实webpack关于缓存方面的功能,提供了很多功能强大的插件。

  • 比如CommonsChunkPlugin可以用来在打包的时候提取公共js代码,还有其他插件
  • 使用HappyPack多线程加速loader
  • ExtractTextPlugin可以用来从js中提出css,将其输出到一个独立的文件
  • 能够将我们打包的精度加以划分,将公共引用的部分打包为一个单独的文件
  • 使用chunkhash替代hash,根据模块内容来添加hash,只要文件没有改变,就不会生成新的hash

4.依赖模块采用第三方CDN资源(对于第三方js库的优化,分离打包) 

生产环境是内网的话,就把资源放内网,通过静态文件引入,会比node_modules和外网CDN的打包加载快很多。如果有外网的话,可以通过CDN方式引入,因为不用占用访问外网的带宽,不仅可以为您节省流量,还能通过CDN加速,获得更快的访问速度。但是要注意下,如果你引用的CDN 资源存在于第三方服务器,在安全性上并不完全可控。国内的CDN服务推荐使用 BootCDN

目前采用引入依赖包生产环境的js文件方式加载,直接通过window可以访问暴露出的全局变量,不必通过import引入,Vue.use去注册

在webpack的dev开发配置文件中, 加入如下参数,可以分离打包第三方资源包,key为依赖包名称,value是源码抛出来的全局变量。对于一些其他的工具库,尽量采用按需引入的方式。

使用 CDN 的好处有以下几个方面

(1)加快打包速度。分离公共库以后,每次重新打包就不会再把这些打包进 vendors 文件中。
(2)CDN减轻自己服务器的访问压力,并且能实现资源的并行下载。浏览器对 src 资源的加载是并行的(执行是按照顺序的)。

第一步:修改vue.config.js

module.exports = {
    ...
    externals: {
        'vue': 'Vue',
        'vuex': 'Vuex',
        'vue-router': 'VueRouter',
        'axios': 'axios',
        'element-ui': 'ELEMENT',
        'underscore' : {
          commonjs: 'underscore',
          amd: 'underscore',
          root: '_'
        },
        'jquery': {
          commonjs: 'jQuery',
          amd: 'jQuery',
          root: '$'
        }
    }    
    ...
}

如果想引用一个库,但是又不想让webpack打包,且又不影响我们在程序中以CMD、AMD或者window/global全局等方式进行使用,那就可以通过配置externals

第二步:在index.html中添加cdn资源文件的具体版本

    <link href="https://cdn.bootcss.com/element-ui/2.7.2/theme-chalk/index.css" rel="stylesheet">
  </head>

  <body>

    <div id="app"></div>

    <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
    <script src="https://cdn.bootcss.com/vuex/3.1.0/vuex.min.js"></script>
    <script src="https://cdn.bootcss.com/vue-router/3.0.4/vue-router.min.js"></script>
    <script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>
    <script src="https://cdn.bootcss.com/element-ui/2.7.2/index.js"></script>

    <script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.min.js"></script>
    <script src="https://cdn.bootcss.com/underscore.js/1.9.1/underscore-min.js"></script>

  </body>

第三步:去除vue.use相关代码

通过 CDN 引入,在使用 VueRouter Vuex ElementUI 的时候要改下写法。CDN会把它们挂载到window上,可以不再使用Vue.use(xxx)

main.js中 注释掉

// import Vue from 'vue';
// import iView from 'iview';
// import '../theme/index.less';

5.生产环境禁止生成map文件

vue.config.js配置:

module.exports = {
    productionSourceMap: process.env.NODE_ENV === 'production' ? false : true, // 生产环境是否生成 sourceMap 文件,一般情况不建议打开
}

在设置了productionSourceMap: false之后,就不会生成map文件,map文件的作用在于:项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错。也就是说map文件相当于是查看源码的一个东西。如果不需要定位问题,并且不想被看到源码,就把productionSourceMap 置为false,既可以减少包大小,也可以加密源码。

6.去掉代码中的console和debugger

打包之后控制台很干净,部署正式环境之前最好这样做。vue-cli3.0

configureWebpack: config => {
    if (process.env.NODE_ENV === 'production') {
      config.optimization.minimizer[0].options.terserOptions.compress.warnings = false
      config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true
      config.optimization.minimizer[0].options.terserOptions.compress.drop_debugger = true
      config.optimization.minimizer[0].options.terserOptions.compress.pure_funcs = ['console.log']
    }
  },

uglifyOptions去除console来减少文件大小

// 安装uglifyjs-webpack-plugin
cnpm install uglifyjs-webpack-plugin --save-dev
 
// 修改vue.config.js
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
    configureWebpack: (config) => {
        if (process.env.NODE_ENV === 'production') {
            return {
                plugins: [
                    //打包环境去掉console.log
                    new UglifyJsPlugin({
                        uglifyOptions: {
                            compress: {
                                warnings: false,
                                drop_console: true,  //注释console
                                drop_debugger: true, //注释debugger
                                pure_funcs: ['console.log'], //移除console.log
                            },
                        },
                    }),
                ],
            }
        }
    }
}

7. 资源加载优化 prefetch(预加载)、preload(按需加载,默认)

prefetch会等到preload的资源加载后在加载 

    config.plugins.delete('preload')        关掉preload(默认是preload加载)

    config.plugins.delete('prefetch')       关掉prefetch

 vue.config.js 中全局配置中关了prefetch,某个组件开启prefetch 如下:

webpackPrefetch: true        同时加载

webpackPrefetch: 数值       加载的优先级,数值越大优先级越高

使用插件:prerender-spa-plugin 

vue.config.js中配置如下:

const PrerenderSpaPlugin = require('prerender-spa-plugin');
const Render = PrerenderSpaPlugin.PuppeteerRenderer;
const path = require('path');
 
configureWebpack: () => {
  if (process.env.NODE_ENV !== 'production') return;
  return {
    plugins: [
      new PrerenderSPAPlugin({
        // 生成文件的路径,也可以与webpakc打包的一致。
        // 下面这句话非常重要!!!
        // 这个目录只能有一级,如果目录层次大于一级,在生成的时候不会有任何错误提示,在预渲染的时候只会卡着不动。
        staticDir: path.join(__dirname, 'dist'),
 
        // 对应自己的路由文件,比如a有参数,就需要写成 /a/param1。
        routes: ['/', '/Login', '/Home'],
 
        // 这个很重要,如果没有配置这段,也不会进行预编译
        renderer: new Renderer({
          inject: {
            foo: 'bar'
          },
          headless: false,
          // 在 main.js 中 document.dispatchEvent(new Event('render-event')),两者的事件名称要对应上。
          renderAfterDocumentEvent: 'render-event'
        })
      })
    ]
  };
}

提前加载: Vue 3的路由支持预加载功能,可在用户浏览站点时预先加载下一个页面所需的资源。这将确保用户切换页面时的迅速加载和呈现。 

const routes = [
  {
    path: '/home',
    component: () => import('./Home.vue'),
    meta: { preload: true }
  },
  // 其他路由...
];

8.图片资源的压缩、icon资源使用、雪碧图、代码压缩

严格说来这一步不算在编码技术范围内,但是却对页面的加载速度影响很大。对于所有的图片文件,都可以在一个叫tinypng的网站上去压缩一下。网址:tinypng.com/,对页面上使用到的icon,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http请求压力。然后通过操作CSS的background属性,控制背景的位置以及大小,来展示需要的部分。

// 图片压缩设置
  chainWebpack: config => {
    // 图片打包压缩,使用了 --- image-webpack-loader --- 插件对图片进行压缩
    config.module
      .rule('images')
      .use('image-webpack-loader')
      .loader('image-webpack-loader')
      .options({ bypassOnDebug: true })
      .end()
  },
使用字体图标

字体图标的优点:

1)轻量级:一个图标字体要比一系列的图像要小。一旦字体加载了,图标就会马上渲染出来,减少了 http 请求

2)灵活性:可以随意的改变颜色、产生阴影、透明效果、旋转等

3)兼容性:几乎支持所有的浏览器,请放心使用

图片转 base64 格式

将小图片转换为 base64 编码字符串,并写入 HTML 或者 CSS 中,减少 http 请求

转 base64 格式的优缺点:

1)它处理的往往是非常小的图片,因为 Base64 编码后,图片大小会膨胀为原文件的 4/3,如果对大图也使用 Base64 编码,后者的体积会明显增加,即便减少了 http 请求,也无法弥补这庞大的体积带来的性能开销,得不偿失

2)在传输非常小的图片的时候,Base64 带来的文件体积膨胀、以及浏览器解析 Base64 的时间开销,与它节省掉的 http 请求开销相比,可以忽略不计,这时候才能真正体现出它在性能方面的优势

项目可以使用 url-loader 将图片转 base64:

// 需先安装image-webpack-loader
// npm install image-webpack-loader --save-dev

{
  test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
  use:[
    {
    loader: 'url-loader',
    options: {
      limit: 10000, // 图片小于10k的转为base64格式,超过 file-loader处理
      name: utils.assetsPath('img/[name].[hash:7].[ext]')
      }
    },
    {
      loader: 'image-webpack-loader',// 压缩图片
      options: {
        bypassOnDebug: true,
      }
    }
  ]
}

需要注意一点,上面说到如果图片小于10k将转为base64格式。这里判断的图片大小是经过image-webpack-loader压缩后的图片大小。 

使用cdn服务器单独存储图片
js代码压缩- - - -(webpack 自UglifyJsPlugin插件压缩js文件)
module.exports = {
  optimization: {
    minimizer: [ // 自定义压缩器
      new UglifyJsPlugin({
        uglifyOptions: {
          output: {
            comments: false,	// 删除所有注释
          },
        },
      }),
    ],
  },
};
css 代码压缩- - - - (采用optimize-css-assets-webpack-plugin插件来压缩css代码)
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
      new OptimizeCSSAssetsPlugin({
        cssProcessorOptions: {
          discardComments: {
            removeAll: true,
          },
        },
      }),
    ],
  },
};
压缩HTML---使用html-webpack-plugin压缩HTML
// 在webpack.config.js里使用
const HtmlWebpackPlugin = require('html-webpack-plugin')

//code

plugins:[
  new HtmlWebpackPlugin({
     minify:{
       	//是否对大小写敏感,默认false
        caseSensitive: true,
        //是否简写boolean格式的属性如:disabled="disabled" 简写为disabled  默认false
        collapseBooleanAttributes: true,
        //是否去除空格,默认false
        collapseWhitespace: true,
        //是否压缩html里的css(使用clean-css进行的压缩) 默认值false;
        minifyCSS: true,
        //是否压缩html里的js(使用uglify-js进行的压缩)
        minifyJS: true,
        //Prevents the escaping of the values of attributes
        preventAttributesEscaping: true,
        //是否移除属性的引号 默认false
        removeAttributeQuotes: true,
        //是否移除注释 默认false
        removeComments: true,
        //从脚本和样式删除的注释 默认false
        removeCommentsFromCDATA: true,
        //是否删除空属性,默认false
        removeEmptyAttributes: true,
        //  若开启此项,生成的html中没有 body 和 head,html也未闭合
        removeOptionalTags: false, 
        //删除多余的属性
        removeRedundantAttributes: true, 
        //删除script的类型属性,在h5下面script的type默认值:text/javascript 默认值false
        removeScriptTypeAttributes: true,
        //删除style的类型属性, type="text/css" 同上
        removeStyleLinkTypeAttributes: true,
        //使用短的文档类型,默认false
        useShortDoctype: true
     }, 
     hash:true,                            // 加入哈希来禁止缓存
     template:'./src/index.html',          // 源模板
     filename:'assets/admin.html'          // 编译后的文件及路径 
  })
]

9. 前端页面代码层面的优化

  1. 合理使用v-if和v-show

  2. 合理使用watch和computed

  3. 使用v-for必须添加key, 最好为唯一id, 避免使用index, 且在同一个标签上,v-for不要和v-if同时使用

  4. 定时器的销毁。可以在beforeDestroy()生命周期内执行销毁事件;也可以使用$once这个事件侦听器,在定义定时器事件的位置来清除定时器。详细见vue官网

  5. 长列表性能优化

  6. 图片资源懒加载
  7. 前端接口防止重复请求实现方案-优快云博客
  8. 用innerHTML代替dom操作,减少dom操作的次数,优化js性能
  9. 合理使用requestAnimationFrame动画代替setTimeOut
  10. 通过创建文档碎片 document.createDocumentFragment()-创建虚拟dom来更新dom
  11. 合理使用 Tree shaking

合理使用 Tree shaking

    在webpack中,可以通过在配置文件中设置 optimization.minimize 为true来开启Tree Shaking。 

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

// util.js
export function targetType(target) {
  return Object.prototype.toString.call(target).slice(8, -1).toLowerCase();
}
export function deepClone(target) {
  return JSON.parse(JSON.stringify(target));
}

项目中只使用了 targetType 方法,但未使用 deepClone 方法,项目打包后,deepClone 方法不会被打包到项目里

tree-shaking 原理:

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

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

Tree shaking 并不是万能的

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

// util.js
export default {
  targetType(target) {
    return Object.prototype.toString.call(target).slice(8, -1).toLowerCase();
  },
  deepClone(target) {
    return JSON.parse(JSON.stringify(target));
  }
};

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

同样的,项目中只使用了 targetType 方法,未使用 deepClone 方法,项目打包后,deepClone 方法还是被打包到项目里

在 dist 文件中搜索 deepClone 方法:

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

这也是函数式编程越来越火的原因,因为可以很好利用 tree-shaking 精简项目的体积,也是 vue3 全面拥抱了函数式编程的原因之一

10.利用浏览器缓存;静态资源本地缓存

后端返回资源问题:

  • 采用HTTP缓存,设置Cache-ControlLast-ModifiedEtag等响应头

  • 采用Service Worker离线缓存

前端合理利用localStorage

11.首屏数据尽量并行,如果可行让小数据量接口合并到其他接口 

12.静态资源尽量用多个子域名

13.vue项目中多 import 优化

在Vue项目中进行多 import 优化,主要是为了减少初始加载时间,通过代码分割等方式来按需加载组件和库。以下是一些优化的策略和示例代码:

1 使用异步组件
const AsyncComp = () => import('./AsyncComp.vue');
 
export default {
  components: {
    AsyncComp
  }
}
2 使用webpack的代码分割(code splitting)特性
// 在webpack.config.js中配置
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};
3 按需加载第三方库(例如使用webpack的动态导入)
// 在需要的时候才加载库
import('some-library').then(library => {
  // 使用library
});
4 使用vue-router的懒加载
const Foo = () => import('./Foo.vue');
const Bar = () => import('./Bar.vue');
 
const router = new VueRouter({
  routes: [
    { path: '/foo', component: Foo },
    { path: '/bar', component: Bar }
  ]
});
5 使用懒加载时结合webpack的magic comment
// 使用webpack提供的magic comment
const SomeLibrary = () => import(/* webpackChunkName: "group-name" */ 'some-library');
6 使用按需加载时结合webpack的预加载
webpackPrefetch
// 使用webpack提供的webpackPrefetch
const SomeLibrary = () => import(/* webpackChunkName: "group-name" */ /* webpackPrefetch: 10 */ 'some-library');

14.解决白屏,体验优化

 把上面所有的优化都做完之后,加载速度有了显著提升,但是在网慢的时候还是会有白屏,所以再白屏期间加骨架屏和loading就显得格外重要了。

当我们的js加载完成之后我们的img就会被覆盖了,所以这种效果是特别好的,因为静态资源是在我们的项目当中直接有的,所以加载效率会特别的快,所以尽量让图片的大小变小这样会更好的提高项目效率 

骨架屏插件

这里以 vue-skeleton-webpack-plugin 插件为例,该插件的亮点是可以给不同的页面设置不同的骨架屏,这点确实很酷

1)安装

npm i vue-skeleton-webpack-plugin

2)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' }
          ]
        }
      })
      ]
   }
}

3)新建 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>
  `,
});

补充知识:

1.Webpack中的[name]、[chunkhash]、[id]

在Webpack中,你可以通过配置output.filename来给输出的文件名添加hash。Webpack支持多种hash算法,包括md5, sha256, sha1, 和 sha512。

先来说一下哈希值的不同:

  • hash 是 build-specific 项目hash,即每次编译都不同——适用于开发阶段
  • chunkhash‌:基于整个chunk的内容生成哈希值。一个chunk通常包括多个模块,甚至可能包括其他chunk。因此,当某个chunk的内容发生变化时,整个chunk的文件名都会发生变化。
    chunkhash 适合用于那些将代码分割成多个 chunk 的项目(如使用代码分割、懒加载等技术)。它可以更细粒度地控制缓存,因为只有改变了的 chunk 会失去缓存。
  • ‌contenthash‌:基于单个文件内容生成哈希值。它只关注文件的内容,不管这个文件是由哪个chunk生成的。因此,只有当文件内容发生变化时,文件名才会发生变化。
    contenthash 特别适合用于样式文件(如 CSS)或者你的项目中任何可以独立缓存的资源。

所以,在生产环境,要把文件名改成'[name].[chunkhash]',利用内容哈希解决浏览器缓存问题

使用chunkhash替代hash,根据模块内容来添加hash,只要文件没有改变,就不会生成新的hash 

// webpack.config.js
const path = require('path');

module.exports = {
    // ...
    output: {
        ...........
        // 使用[contenthash]来生成唯一的文件名
        filename: "[name].[contenthash].js", // 使用contenthash而非chunkhash,适用于小文件
        path: path.resolve(__dirname, 'dist'),
        // 对于代码分割的chunk文件也应用相同的策略
        chunkFilename: '[name].[contenthash].chunk.js',
    },
  // ...
};

在这个配置中,[name]是文件的名字(通常是入口点的名字),[contenthash]是文件内容的hash,它会在文件内容变化时变化,从而确保缓存的有效性。

如果你想要使用chunkhash,它会根据每个chunk(bundle)的内容计算hash,适用于大文件:

// webpack.config.js
const path = require('path');
 
module.exports = {
  // ...
  output: {
    filename: '[name].[chunkhash:8].js', // 使用chunkhash;8 表示hash值的长度
    path: path.resolve(__dirname, 'dist'),
  },
  // ...
};

2.Brotli 的性能比Gzip提高了 17-25%

Brotli 压缩只在 https 下生效,因为在 http 请求中 request header里的 Accept-Encoding 只有gzip,deflate。并且 Brotli 和 Gzip 是可以并存的,因此无需关闭gzip,客户端自行判断压缩算法

使用的前提

在 Chrome 中只有 https 的网站发起的请求头 Accept-Encoding 中才会加上 br 选项,此时服务端才会响应 brotli 压缩的资源。因此只有网站支持 https 时,开启 br 压缩才有价值

1、Brotli 简介

Brotli 是 Google 推出的一种无损压缩算法,通过变种的LZ77算法、Huffman编码以及等方式进行数据压缩。Brotli压缩数据格式的规范在RFC 7932中定义。与其他压缩算法相比(如zip,gzip等),无论是压缩时间,还是压缩体积上看,它都有着更高的效率。

Brotli压缩支持单文件

  • 支持的文件类型: text/xml、text/plain、text/css、text/javascript、application/javascript、application/x-javascript、application/rss+xml、application/json、application/xml、image/tiff、image/svg+xml
  • 可结合tar, 达到支持文件夹

2、Brotli优势

HTTP的gzip压缩可节省网络流量,提升网络速度,但gzip压缩率还不够高,通过调研,有大量算法压缩率都比gzip高。综合压缩率和实现成本,brotli压缩算法落地成本最低,且业界字节app已有多个域名使用brotli算法。
brotli有11个级别,通过测试发现选用中间级别—7级别比较合适,brotli相对gzip消耗的CPU和内存资源更多,但在可接受范围内。
参考链接:https://juejin.cn/post/7194627379656917047

3、Brotli接入到android

对于 web 和 ios 端,在浏览器和系统层已经开始支持 br 的自动解压了,但 Android 系统不会自动解析 br 数据,需要在应用层自己去实现。
Okhttp 中发现了 BrotliInterceptor 的拦截器,但它是 Brotli 库的 Java 描述,没有真正的 Java 编码器实现。

1. 首先请求头中加入Content-Encoding:br

val request = chain.request().newBuilder()
    .header("Accept-Encoding", "br,gzip")
    .build()

2. OkHttp请求的时候添加拦截器

  1. 直接使用square公司的拦截器
    implementation("com.squareup.okhttp3:okhttp-brotli:4.1.0")
    OkHttpClient client = new OkHttpClient.Builder()
      .addInterceptor(BrotliInterceptor.INSTANCE)
      .build();
  2. 依赖implementation("org.brotli:dec:0.1.2”),自行实现拦截器
     implementation("org.brotli:dec:0.1.2") //https://mvnrepository.com/artifact/org.brotli/dec
    

    拦截器

    object BrotliInterceptor : Interceptor {
      override fun intercept(chain: Interceptor.Chain): Response {
        return if (chain.request().header("Accept-Encoding") == null) {
          val request = chain.request().newBuilder()
            .header("Accept-Encoding", "br,gzip")
            .build()
          val response = chain.proceed(request)
          uncompress(response)
        } else {
          chain.proceed(chain.request())
        }
      }
      fun uncompress(response: Response): Response {
      if (!HttpHeaders.hasBody(response)) {
        	return response
        }
    	val body = response.body() ?: return response
      	val encoding = response.header("Content-Encoding") ?: return response
      	val decompressedSource = when {
        encoding.equals("br", ignoreCase = true) ->
          BrotliInputStream(body.source().inputStream()).source().buffer()
        encoding.equals("gzip", ignoreCase = true) ->
          GzipSource(body.source()).buffer()
        else -> return response
      }
    
      return response.newBuilder()
        .removeHeader("Content-Encoding")
        .removeHeader("Content-Length")
        .body(decompressedSource.asResponseBody(body.contentType(), -1))
        .build()
      }
    }
    

4、怎么使用Brotli压缩

怎样知道浏览器是否支持 Brotli?

当打开一个网页,然后在浏览器使用期开发工具的时候,可以查看其 content-encoding,如果看到 br 字样,那就是Brotli,如果看到 gzip,那就是用的 gzip 方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值