首屏渲染

161 阅读6分钟

1.问题简述

项目搭建完成后,首次加载速度缓慢,严重影响交互

2.分析优化

多余依赖:

项目采用jeecg2.0作为基础框架,原本的框架包含众多不被需要的依赖。

首先删除不需要的页面路由文件,逐个查看依赖的引用关系,删除相关无用页面与无引用依赖

moment.js仅保留中文

//只保留中文语言包,chainWebpack下
chainWebpack: (config) => {
    config.plugin('ContextReplacementPlugin').use(webpack.ContextReplacementPlugin, [/moment[/\]locale$/, /zh-cn/])
 }

@ant icons按需加载

Ant Design Vue 1.2.x起,icon偏大,推荐按需引入

chainWebpack: (config) => {
    config.resolve.alias
      .set('@ant-design/icons/lib/dist$', resolve('src/icons.js'))
 }
// src/icons.js
export {
  ReloadOutline,
  CloseCircleFill,
  ExclamationCircleFill,
  CloseCircleOutline,
  CalendarOutline,
  SearchOutline,
  PlusOutline,
  ExportOutline,
  ImportOutline,
  DownloadOutline,
  HddOutline,
  FilterOutline,
  SyncOutline,
  InfoCircleFill,
  DownOutline,
  LockOutline,
  UnlockOutline,
  DeleteOutline,
  QuestionCircleOutline,
  CheckCircleOutline,
  CloseOutline,
  GoldOutline,
  CaretDownFill,
  CaretUpFill
} from '@ant-design/icons'

echarts按需加载

// utils/charts.js

// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。
import * as echarts from 'echarts/core'

/** 引入柱状图and折线图图表,图表后缀都为 Chart  */
import { BarChart, LineChart, PieChart } from 'echarts/charts'

// 引入提示框,标题,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component
import {
  TitleComponent,
  TooltipComponent,
  GridComponent,
  DatasetComponent,
  TransformComponent,
  LegendComponent,
  DataZoomComponent,
  GraphicComponent
} from 'echarts/components'

// 标签自动布局,全局过渡动画等特性
import { LabelLayout, UniversalTransition } from 'echarts/features'

// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
import { CanvasRenderer } from 'echarts/renderers'

// 注册必须的组件
echarts.use([
  TitleComponent,
  TooltipComponent,
  GridComponent,
  DatasetComponent,
  TransformComponent,
  BarChart,
  LabelLayout,
  UniversalTransition,
  CanvasRenderer,
  LineChart,
  LegendComponent,
  DataZoomComponent,
  GraphicComponent,
  PieChart
])

// 导出
export default echarts
// 引入
import echarts from '@utils/charts.js'

splitchunks拆包

拆包并不能直接减小打包体积,原理是将原本一个整体,拆成多个个体,借助浏览器可创建多个下载线程的能力实现并行加载。(在项目部分资源偏大明显,即拉低或均匀整体资源加载的峰值时间)

splitChunks: {    
// 表示选择哪些 chunks 进行分割,可选值有:async,initial和all    
chunks: "async",    
// 表示新分离出的chunk必须大于等于minSize,注:新分离,原本小于30的可以小于30
minSize: 30 * 1024,  
// 表示新分离出的chunk必须小于maxSize,注:同上,且存在单个文件就大于配置的,也不进行拆分
maxSize: 100 * 1024,  
// 表示一个模块至少应被minChunks个chunk所包含才能分割。默认为1。    
minChunks: 1,    
// 表示按需加载文件时,并行请求的最大数目。默认为5。    
maxAsyncRequests: 5,    
// 表示加载入口文件时,并行请求的最大数目。默认为3。    
maxInitialRequests: 3,    
// 表示拆分出的chunk的名称连接符。默认为~。如chunk~vendors.js    
automaticNameDelimiter: '~',    
// 设置chunk的文件名。默认为true。当为true时,splitChunks基于chunk和cacheGroups的key自动命名。    
name: true,    
// cacheGroups 下可以可以配置多个组
// 每个组根据test设置条件,符合test条件的模块,就分配到该组。
// 模块可以被多个组引用,但最终会根据priority来决定打包到哪个组中。
// 默认将所有来自 node_modules目录的模块打包至vendors组,将两个以上的chunk所共享的模块打包至default组。    
cacheGroups: {        
    vendors: {            
        test: /[\/]node_modules[\/]/,            
        priority: -10
    },        
//    default: 
    {            
        minChunks: 2,            
        priority: -20,            
        reuseExistinGChunk: true}
    }
}
// chainWebpack下
config.optimization.splitChunks({
      chunks: 'all',
      // 以下布置拆包内容

      cacheGroups: {
        jspdf: {
          name: 'chunk-jspdf',
          priority: 30,
          test: /[\/]node_modules[\/]_?jspdf(.*)/
        },
        echarts: {
          name: 'chunk-echarts',
          priority: 30,
          test: /[\/]node_modules[\/]_?echarts(.*)/
        },
        ant: {
          name: 'chunk-ant',
          priority: 50,
          test: /[\/]node_modules[\/]_?@ant-design|ant-design-vue|@antv(.*)/
        }
      }
    })

合并打包

大多数时候我们不需要追究极限的性能需求,但如果你希望

举例:

假设拆包结果:

  Antd : 600k

  Moment : 80k

  app : 200k

此时我们最大的单元包就是按需加载时的antd,

其余包最大的也不超过antd体积的1/3,

这时问题出现,antd本身是否还可以进行拆包,进行二度均分

答案是可以,但是由于antd本身已经是单元依赖

我们需要通过限制最大包体积maxSize进行大小拆分(即打散),可以拆分成1-几百个极小单元

此时空间性能转化为了多网络请求的时间性能,我们需要再对散包合并,即合并打包

注意:

合并打包能力与拆包存在顺序关系

为避免出现不合预期的效果,推荐先拆再合

// 通过限制minChunkSize,和maxChunks 能打出很多情况,选取你自身期望的效果
config.plugin('chunkPlugin').use(webpack.optimize.LimitChunkCountPlugin, [{
        maxChunks: 5, 
        minChunkSize: 10000
}])

Babel-plugin-import 按需ant-design

原理:在你按需引入ant组件时隐性加载对应样式,减小体积

// 手动按需
import Button from 'vant/lib/button';
import 'vant/lib/button/style';

// babel自动按需
import { Button } from 'vant';
Vue.use(Button);
// 等同于手动按需
// 安装
npm install babel-plugin-import 

module.exports = {
  presets: [['@vue/app', { useBuiltIns: 'entry' }]],
  plugins: [['import', { libraryName: 'ant-design-vue', libraryDirectory: 'es', style: 'css' }]]
}

font-spider压缩字体文件(如果字体文件使用量少且内容固定)

引用字体文件偏大时,如果只有例如首页标题等固定少量字体使用,可以打包仅包含该字体的字体文件

例如:用了dnn字体,dnn字体包2M,只打包dnn则只有30k。

暂时无法在飞书文档外展示此内容

// 安装(演示包在上面,需要的话自行下载)
npm install font-spider -g
// 运行
font-spider *.html

组件全局注册组件

main.js中全局注册的组件,及其相关依赖固定会在首屏渲染时引用(不受路由按需加载等影响,固定初次加载时引用)。

对部分引用较少或不需要全局注册的组件,放到对应的页面中引用,保证其页面使用时再加载对应依赖

即如下方式:

不推荐全局注册组件(下面的例子是不推荐写法)

可以考虑根据引用情况,下放到实际引用的组件处导入

import JModal from './JModal';
import JFormContainer from './JFormContainer.vue';
import JTreeSelect from './JTreeSelect.vue';
import JImageUpload from './JImageUpload.vue';
import JDate from './JDate.vue';
import JEllipsis from './JEllipsis.vue';
import JInput from './JInput.vue';
import JEasyCron from '@/components/jeecg/JEasyCron';
// jeecgbiz
import JSelectDepart from '../jeecgbiz/JSelectDepart.vue';
import JSelectMultiUser from '../jeecgbiz/JSelectMultiUser.vue';
import JSelectPosition from '../jeecgbiz/JSelectPosition.vue';
import JSelectUserByDep from '../jeecgbiz/JSelectUserByDep.vue';
// 引入需要全局注册的js函数和变量
import { Modal, notification } from 'ant-design-vue';

export default {
  install(Vue) {
    Vue.use(JModal);
    Vue.component('JDate', JDate);
    Vue.component('JEllipsis', JEllipsis);
    Vue.component('JFormContainer', JFormContainer);
    Vue.component('JImageUpload', JImageUpload);
    Vue.component('JInput', JInput);
    Vue.component('JTreeSelect', JTreeSelect);

    // jeecgbiz
    Vue.component('JSelectDepart', JSelectDepart);
    Vue.component('JSelectMultiUser', JSelectMultiUser);
    Vue.component('JSelectPosition', JSelectPosition);
    Vue.component('JSelectUserByDep', JSelectUserByDep);
    Vue.component(JEasyCron.name, JEasyCron);

    // 注册全局js函数和变量
    Vue.prototype.$Jnotification = notification;
    Vue.prototype.$Jmodal = Modal;
  },
};

区分路由按需引入

该框架中采用require的方式进行路由页面引入

const componentPath = (resolve) => require([`@/${component}.vue`], resolve);

使用这种方式引入对应页面的组件vue时发现并不能根据路由引入依赖,而是初次加载时就加载全部依赖,需要更改为import引入,如果你的项目已经使用了import方式,请不要修改。

// 生成嵌套路由(子路由)

function generateChildRouters(data) {
  const routers = [];
  for (const item of data) {
    let component = '';
    if (item.component.indexOf('layouts') >= 0) {
      component = `components/${item.component}`;
    } else {
      component = `views/${item.component}`;
    }

    // eslint-disable-next-line
    let URL = (item.meta.url || '').replace(/{{([^}}]+)?}}/g, (s1, s2) => eval(s2)); // URL支持{{ window.xxx }}占位符变量
    if (isURL(URL)) {
      item.meta.url = URL;
    }

    // const componentPath = (resolve) => require([`@/${component}.vue`], resolve);

    const menu = {
      path: item.path,
      name: item.name,
      redirect: item.redirect,
      hidden: item.hidden,
      component: () => import(`@/${component}.vue`), // 这里!!!!!!!!!!!
      meta: {
        title: item.meta.title,
        icon: item.meta.icon,
        url: item.meta.url,
        permissionList: item.meta.permissionList,
        keepAlive: item.meta.keepAlive,
        /* update_begin author:wuxianquan date:20190908 for:赋值 */
        internalOrExternal: item.meta.internalOrExternal,
        /* update_end author:wuxianquan date:20190908 for:赋值 */
        componentName: item.meta.componentName,
      },
    };
    if (item.alwaysShow) {
      menu.alwaysShow = true;
      menu.redirect = menu.path;
    }
    if (item.children && item.children.length > 0) {
      menu.children = [...generateChildRouters(item.children)];
    }
    if (!item.route || item.route !== '0') {
      routers.push(menu);
    }
  }
  return routers;
}

nginx压缩

资源压缩能大量的减少时间,但本质属于运维配置项,需要沟通运维人员

# gzip config
gzip on;#开启压缩
gzip_min_length 1k;#低于1k的不压缩
gzip_comp_level 9;#压缩程度
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;#对哪些mine资源开启Gzip压缩功能
gzip_vary on;#是否在响应报文首部插入“Vary: Accept-Encoding”
gzip_disable "MSIE [1-6].";#针对不同种类客户端发起的请求,选择性地关闭Gzip功能,这里意思是禁用IE6 gzip功能

手动prefetch

webpack会自动创建prefetch链接,增加首屏渲染时间

如果需要进行prefetch,推荐通过prefetch标签手动管理

// prefetch是一种 resource hint,
// 用来告诉浏览器在页面加载完成后,利用空闲时间提前获取用户未来可能会访问的内容。
// 移除 prefetch避免加载多余的资源
config.plugins.delete('prefetch')

3.vue.config.js样例

const webpack = require('webpack');
const path = require('path');
const CompressionPlugin = require('compression-webpack-plugin');

function resolve(dir) {
  return path.join(__dirname, dir);
}

// vue.config.js
module.exports = {
  /*
    Vue-cli3:
    Crashed when using Webpack `import()` #2463
    https://github.com/vuejs/vue-cli/issues/2463
   */
  // 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
  productionSourceMap: false,
  // qiankuan打包时放开
  // outputDir: "../dist/main",
  // 多入口配置
  // pages: {
  //   index: {
  //     entry: 'src/main.js',
  //     template: 'public/index.html',
  //     filename: 'index.html',
  //   }
  // },
  // 打包app时放开该配置
  publicPath: './',
  configureWebpack: (config) => {
    // 生产环境取消 console.log
    if (process.env.NODE_ENV === 'production') {
      config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true;
    }
  },
  chainWebpack: (config) => {
    config.plugins.delete('prefetch');

    config.resolve.alias
      .set('@$', resolve('src'))
      .set('@api', resolve('src/api'))
      .set('@assets', resolve('src/assets'))
      .set('@comp', resolve('src/components'))
      .set('@views', resolve('src/views'))
      .set('@utils', resolve('src/utils'))
      .set('@ant-design/icons/lib/dist$', resolve('src/icons.js'));
    // 生产环境,开启js\css压缩
    if (process.env.NODE_ENV === 'production') {
      config.plugin('compressionPlugin').use(
        new CompressionPlugin({
          test: /.(js|css|less)$/, // 匹配文件名
          threshold: 10240, // 对超过10k的数据压缩
          deleteOriginalAssets: false, // 不删除源文件
        }),
      );
      //只保留中文语言包
      config
        .plugin('ContextReplacementPlugin')
        .use(webpack.ContextReplacementPlugin, [/moment[/\]locale$/, /zh-cn/]);

      config.optimization.splitChunks({
        chunks: 'all'
      });
    }

    // 配置 webpack 识别 markdown 为普通的文件
    config.module.rule('markdown').test(/.md$/).use().loader('file-loader').end();
  },

  css: {
    loaderOptions: {
      less: {
        modifyVars: {
          /* less 变量覆盖,用于自定义 ant design 主题 */
          'primary-color': '#3D6EFF',
          'link-color': '#3D6EFF',
          'border-radius-base': '2px',
        },
        javascriptEnabled: true,
      },
    },
  },

  devServer: {
    port: 3000,
    // hot: true,
    // disableHostCheck: true,
    // overlay: {
    //     warnings: false,
    //     errors: true,
    // },
    // headers: {
    //     'Access-Control-Allow-Origin': '*',
    // },
    proxy: {
      /* '/api': {
        target: 'https://mock.ihx.me/mock/5baf3052f7da7e07e04a5116/antd-pro', //mock API接口系统
        ws: false,
        changeOrigin: true,
        pathRewrite: {
          '/jeecg-boot': ''  //默认所有请求都加了jeecg-boot前缀,需要去掉
        }
      }, */
      /* 注意:jeecgboot前端做了改造,此处不需要配置跨域和后台接口(只需要改.env相关配置文件即可)
          issues/3462 很多人此处做了配置,导致刷新前端404问题,请一定注意 */
      '/jeecg-boot': {
        target: 'http://localhost:8080',
        ws: false,
        changeOrigin: true,
      },
    },
  },

  lintOnSave: undefined,
};