前端加载优化之速度优化解决方案

8,321 阅读9分钟

随着前端各种框架的日益完善,一些基本的性能优化和加载优化都已经很完善了,但是有些必要的优化还是得开发者自己去做。vue.js是一个比较流行的前端框架,与react.js、angular.js相比来说,vue.js入手曲线更加流畅,不管掌握多少都可以快速上手。但是单页面应用也都有其弊病,有时候首屏加载慢的让人捏舌。今天我们以vue cli2.x来说一说如何行之有效的缓解此问题!以项目为例,输入网址以后会出现十几秒的空白页,如果是后台管理系统还能接受,嵌套式的H5面对的是C端用户,产品肯定是无法接受的。仔细分析了下,主要是打包后的app.js太大,以及我们引用的一些插件安装包加载比较慢,在网上搜了很多解决加载慢的方案,最终优化的时间移动端H5页面2秒多,后台管理系统5秒多。下面,将自己在平时项目上所做的优化策略实践分享于大家。文章较长,耐心看完会有收获的。或者可以到我的个人博客里面查看更多的技术

方案一、路由懒加载

首屏加载慢的原因无非就是单页面应用需要加载完整个路由表上的页面,而路由懒加载就是来解决这个问题的。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。下面这个就是vue路由懒加载的一个具体例子。方法很简单,如果您不想深入了解,只需按照这个格式引入路由就可以了。如果您对路由懒加载感兴趣,请移步vue-router路由懒加载。

此方法会把原本打包到一个app.js文件分开成多个js文件打包,这样会减小单个文件的大小,但是不会减小整个js文件夹的大小。

通过这种方式可以做到按需加载,只加载单个页面的js文件。

  {
    path: '/home',
    name: 'home',
    component: r => require.ensure([], function (require) {
      r(require('views/hls/Home/Home.vue'))
    }, 'home'),
    meta: {
      title: '首页',
      keepAlive: false,
      hasTop: true,
      hasBottom: true
    }
  }

方案二、组件按需加载(代码分割)

webpack将打包资源都打包在了一个bundle.js中,其中主要包含了开发的源代码 和 第三方依赖node_modules。 我们可以对node_modules第三方依赖 打包资源拆分细化成多个资源文件,借助浏览器支持HTTP同时发起多个请求特性,使得资源异步并行加载,从而提高资源加载速度。 webpack提供了splitChunks来支持这一配置。

1. 首先引入按需加载工具 babel-plugin-import

babel-plugin-import是babel它会在编译过程中将 import 的写法自动转换为按需引入的方式。

npm install babel-plugin-import --save-dev

2. 在项目根目录创建.babelrc文件并配置按需加载内容:

{
 "plugins": [["import", {
  "libraryName": "iview",
  "libraryDirectory": "src/components"
 }]]
}

3. webpack配置:

// webpack.config.js
module.exports = function ({ production = false, development = false }) {
  ...
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'static/js/[name].[contenthash:8].js', // 主文件分割出的文件命名
    chunkFilename: 'static/js/[name].[contenthash:8].chunk.js', // splitChunks 分割出的文件命名
  },
  
  optimization: {
      minimize: true,
      ...
      splitChunks: {
        chunks: 'all',
        cacheGroups: {
          default: false,
          vendors: false,
          // 多页面应用,或者 webpack import() 多个 chunk 文件中,有 import 其他模块两次或者多次时,会打包生成 common
          common: {
            chunks: "all",
            minChunks: 2,
            name: 'common',
            enforce: true,
            priority: 5
          }, 
          // node_modules 中的公共模块抽离
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            chunks: 'initial',
            enforce: true,
            priority: 10,
            name: 'vendor'
          },
          // @materual-ui
          material: {
            name: 'chunk-material',
            priority: 20, // 优先级高于 vendor
            test: /[\\/]node_modules[\\/]_?@material-ui(.*)/
          },
        }
      },
      runtimeChunk: { // 运行时代码(webpack执行时所需的代码)从主文件中抽离
        name: entrypoint => `runtime-${entrypoint.name}`,
      },
    },
})

4. 在main.js配置项目需要加载的组件

下面是iview的一个例子:

Vue.component("Icon", Icon)
Vue.component("Notice", Notice)
Vue.component("Button", Button)

这里需要注意全局注册的组件需要挂在到vue原型上,例如我们需要使用Notice组件,那我就需要

Vue.prototype.$Notice = Notice;

这样我们就可以正常的使用iview的组件了。

方案三、第三方组件库UI框架,使用按需引入

1. 借助 babel-plugin-component ,引入我们需要的组件,减少项目体积

npm install babel-plugin-component -D

2. 修改 babel.config.js 的内容

//babel.config.js 全文内容如下
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ],
  plugins: [
    [
      'component',
      {
        libraryName: 'element-ui',
        styleLibraryName: 'theme-chalk'
      }
    ]
  ]
}

3. 创建文件 element.js(名字自定义)

// element.js 全文内容如下,按自己需要引入就好了
import Vue from 'vue'
import {
    Button,
    Form,
    FormItem,
    Input,
    Message,
    Container,
    Header,
    Aside,
    Main,
    Menu,
    Submenu,
    MenuItem,
    Breadcrumb,
    BreadcrumbItem,
    Card,
    Row,
    Col,
    Table,
    TableColumn,
    Switch,
    Tooltip,
    Pagination,
    Dialog,
    MessageBox,
    Tag,
    Tree,
    Select,
    Option,
    Cascader,
    Alert,
    Tabs,
    TabPane
} from 'element-ui'
Vue.use(Button)
Vue.use(Form)
Vue.use(FormItem)
Vue.use(Input)
Vue.use(Container)
Vue.use(Header)
Vue.use(Aside)
Vue.use(Main)
Vue.use(Menu)
Vue.use(Submenu)
Vue.use(MenuItem)
Vue.use(Breadcrumb)
Vue.use(BreadcrumbItem)
Vue.use(Card)
Vue.use(Row)
Vue.use(Col)
Vue.use(Table)
Vue.use(TableColumn)
Vue.use(Switch)
Vue.use(Tooltip)
Vue.use(Pagination)
Vue.use(Dialog)
Vue.use(Tag)
Vue.use(Tree)
Vue.use(Select)
Vue.use(Option)
Vue.use(Cascader)
Vue.use(Alert)
Vue.use(Tabs)
Vue.use(TabPane)
Vue.prototype.$message = Message
Vue.prototype.$confirm = MessageBox.confirm

4. 最后在 main.js 中引入这个文件

//main.js 中添加下面这行代码(路径和文件名按自己的来)
import './plugins/element.js'

另外在main.js中需要添加以下引入:

import ElementUI from 'element-ui';
Vue.use(ElementUI);

方案四、使用CDN减小代码体积加快请求速度

在项目开发中,我们会用到很多第三方库,如果可以按需引入,我们可以只引入自己需要的组件,来减少所占空间,

但也会有一些不能按需引入,我们可以采用CDN外部加载,在index.html中从CDN引入组件,去掉其他页面的组件import,修改webpack.base.config.js,在externals中加入该组件,这是为了避免编译时找不到组件报错。

所以使用CDN主要解决两个问题:

  1. 打包时间太长、打包后代码体积太大,请求慢
  2. 服务器网络不稳带宽不高,使用cdn可以回避服务器带宽问题

我们将vue,vue-router,vuex,axios,echarts,element,moment使用CDN资源引入。国内的CDN服务推荐使用bootCDN

1. 在index.html里引入线上cdn,为了方便后续引用其他资源的cdn,所以我们需要把index.html里面的引入cdn配置成动态的。

<html>
  ...
  <body>
    <!-- 使用CDN加速的JS文件,配置在vue.config.js下 -->
    <% for (var i in
    htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
    <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
    <% } %>
  </body>
</html>

2. 在vue.config.js配置中配置是为了在加载的时候,引用cdn资源 而不是node_modules里的包

....
const externals = {
  vue: 'Vue',
  'vue-router': 'VueRouter',
  vuex: 'Vuex',
  axios: 'axios'
}
....
configureWebpack: config => {
    if (isProduction) { // 判断是否是生产环境的包
      Object.assign(config, {
        externals: externals
      })
    }
    config.module.unknownContextCritical = false
  }

同时注销掉main.js文件当中的import

方案五、gzip打包,nginx开启gzip压缩

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

2、之后就是nginx配合开启gzip模式,这个比较简单,只要你对nginx有一点了解,我们在nginx.conf中的http中配置一些代码。

compression-webpack-plugin这个依赖在npm run build是会生成.gz文件。之后项目访问的文件就是这个.gz文件,正常的项目打包体积会减少一半还要多,先看下打包后的文件:

1. webpack安装compression-webpack-plugin插件,新版本有问题的话下载1.1.12版本配置vue.config.js

npm i compression-webpack-plugin --save-dev

vue.config.js配置:

const CompressionWebpackPlugin = require('compression-webpack-plugin')
configureWebpack:{
	plugins:[
      new CompressionWebpackPlugin({
        filename: '[path].gz[query]',
        algorithm: 'gzip',
        // test: /\.js$|\.html$|\.json$|\.css/,
        test: /\.js$|\.json$|\.css/,
        threshold: 10240, // 只有大小大于该值的资源会被处理
        minRatio: 0.8, // 只有压缩率小于这个值的资源才会被处理
        // deleteOriginalAssets: true // 删除原文件
      })
    ],

2. 需要nginx服务器,更改nginx.conf文件, 加在如图所示位置

# 开启gzip。
gzip on
# 开启后如果能找到 .gz 文件,直接返回该文件,不会启用服务端压缩。
gzip_static on
# 文件大于指定 size 才压缩,以 kb 为单位。
gzip_min_length 1;
# 用于识别http协议的版本,早期的浏览器不支持gzip压缩,用户会看到乱码,所以为了支持前期版本加了此选项,目前此项基本可以忽略
gzip_http_version 1.1;
# 压缩级别,1-9,值越大压缩比越大,但更加占用 CPU,且压缩效率越来越低。
gzip_comp_level 9;
# 压缩的文件类型。
gzip_types text/css application/javascript application/json;
root /dist;

修改配置后重新加载生效:nginx -s reload

3. 验证是否配置成功

这步就很简单了只需要查看chunk类文件的Response Headers的Content-Encoding是否是gzip即可

方案六、打包文件中去掉map文件

打包的app.js过大,另外还有一些生成的map文件。先将多余的map文件去掉,找到config文件夹下index.js文件,把这个build里面的productionSourceMap改成false即可。

   /*
   Source Maps
   */
    productionSourceMap: true,  // 把这边的true改为false
    // https://webpack.js.org/configuration/devtool/#production
    devtool: '#source-map',

方案七、压缩代码,移除console.log

npm install uglifyjs-webpack-plugin --save-dev

配置vue.config.js:

const isProduction = process.env.NODE_ENV === 'production';
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
chainWebpack(config) {
    const plugins = [];
    if (isProduction) {
      plugins.push(
        new UglifyJsPlugin({
          uglifyOptions: {
            output: {
              comments: false, // 去掉注释
            },
            warnings: false,
            compress: {
              drop_console: true,
              drop_debugger: false,
              pure_funcs: ['console.log']//移除console
            }
          }
        })
      )
    }
  }

方案八、打包通过image-webpack-loader插件对图片压缩优化

vue正常打包之后一些图片文件很大,使打包体积很大,通过image-webpack-loader插件可将大的图片进行压缩从而缩小打包体积.

1. 先安装webpack依赖插件image-webpack-loader

npm install image-webpack-loader --save-dev

2. 在vue.config.js中module.exports修改

module.exports = {
  productionSourceMap: false,
  chainWebpack: config => {
    // ============压缩图片 start============
    config.module
      .rule('images')
      .use('image-webpack-loader')
      .loader('image-webpack-loader')
      .options({ bypassOnDebug: true })
      .end()
    // ============压缩图片 end============
  }
}

方案十、公共代码的抽离

在vue.config.js module.exports configureWebpack 里面新增,直接放在gzip压缩下边即可

...
// 公共代码抽离
configureWebpack: config => {
  config.optimization = {
    splitChunks: {
      cacheGroups: {
        vendor: {
          chunks: 'all',
          test: /node_modules/,
          name: 'vendor',
          minChunks: 1,
          maxInitialRequests: 5,
          minSize: 0,
          priority: 100
        },
        common: {
          chunks: 'all',
          test: /[\\/]src[\\/]js[\\/]/,
          name: 'common',
          minChunks: 2,
          maxInitialRequests: 5,
          minSize: 0,
          priority: 60
        },
        styles: {
          name: 'styles',
          test: /\.(sa|sc|c)ss$/,
          chunks: 'all',
          enforce: true
        },
        runtimeChunk: {
          name: 'manifest'
        }
      }
    }
  }
}
...

方案十一、Vue预渲染(Prerendering)

服务器端渲染 vs 预渲染 (SSR vs Prerendering)

如果你调研服务器端渲染(SSR)只是用来改善少数营销页面(例如 /, /about, /contact 等)的SEO,那么你可能需要预渲染。无需使用web服务器实时动态编译 HTML,而是使用预渲染方式,在构建时简单地生成针对特定路由的静态HTML文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点。

如果你使用webpack,你可以使用prerender-spa-plugin轻松地添加预渲染。它已经被Vue应用程序广泛测试 - 事实上,作者是Vue核心团队的成员。

三种不同渲染方式的区别:

客户端渲染: 用户访问url,请求html文件,前端根据路由动态渲染页面内容。关键链路较长,有一定的白屏时间;

服务端渲染: 用户访问url,服务端根据访问路径请求所需数据,拼接成 html 字符串,返回给前端。前端接收到html时已有当前url下的完整页面;

预渲染: 构建阶段生成匹配预渲染路径的html文件(注意:每个需要预渲染的路由都有一个对应的 html)。构建出来的html文件已经有静态数据,需要ajax数据的部分未构建。

预渲染解决的问题:

SEO: 单页应用的网站内容是根据当前路径动态渲染的,html文件中往往没有内容,网络爬虫不会等到页面脚本执行完再抓取;

弱网环境: 当用户在一个弱环境中访问你的站点时,你会想要尽可能快的将内容呈现给他们。甚至是在 js 脚本被加载和解析前;

低版本浏览器: 用户的浏览器可能不支持你使用的js特性,预渲染或服务端渲染能够让用户至少能够看到首屏的内容,而不是一个空白的网页。

Vue预渲染实现流程

1. prerender-spa-plugin安装,建议使用淘宝镜像的cnpm,因为npm安装经常失败,否则会出现意向不到的问题,并且效率低,比较浪费时间。

cnpm install prerender-spa-plugin --save-dev

2. vue.config.js配置文件

const path = require('path');
const PrerenderSPAPlugin = require('prerender-spa-plugin');
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;

在plugins下面,找到plugins对象,直接加到上面就行
// 预渲染配置
new PrerenderSPAPlugin({
    //要求-给的WebPack-输出应用程序的路径预渲染。
    staticDir: path.join(__dirname, 'dist'),
    //必需,要渲染的路线。
    routes: ['/login'],
    //必须,要使用的实际渲染器,没有则不能预编译
    renderer: new Renderer({
        inject: {
            foo: 'bar'
        },
        headless: false, //渲染时显示浏览器窗口。对调试很有用。  
        //等待渲染,直到检测到指定元素。
        //例如,在项目入口使用`document.dispatchEvent(new Event('custom-render-trigger'))` 
        renderAfterDocumentEvent: 'render-event'
    })
})

3. router.js配置文件,需要把vue的router模式设置成history模式

4. main.js文件的修改

在创建vue实例的mounted里面加一个事件,跟PrerenderSPAPlugin实例里面的renderAfterDocumentEvent对应上。

mounted () {
    document.dispatchEvent(new Event('render-event'))
 }

这是预渲染的基本配置,npm run build 一下,如果dist文件夹多了你想预渲染的文件夹,那么恭喜你,成功了!如果项目是用nginx做的代理,nginx还需要做一些配置,具体是:

location = / {
  try_files /home/index.html /index.html;
}

location / {
  try_files $uri $uri/ /index.html;
}

5. 相关页面情况