切图妞

vuePress-theme-reco 切图妞    2020 - 2021
切图妞 切图妞
前端知识梳理
  • Vue
  • 浏览器 & 网络
  • HTML & CSS
  • Web安全
  • 算法
文章分类
  • 前端小麻烦
  • 配置乐园
  • 实战不完全手册
  • 手撕源码
宝藏女孩
  • 模板仓
  • 项目简介
  • GitHub
  • Segmentfault
  • CSDN
时间轴
author-avatar

切图妞

19

Article

18

Tag

前端知识梳理
  • Vue
  • 浏览器 & 网络
  • HTML & CSS
  • Web安全
  • 算法
文章分类
  • 前端小麻烦
  • 配置乐园
  • 实战不完全手册
  • 手撕源码
宝藏女孩
  • 模板仓
  • 项目简介
  • GitHub
  • Segmentfault
  • CSDN
时间轴

Vue中后台性能优化方案

vuePress-theme-reco 切图妞    2020 - 2021

Vue中后台性能优化方案

切图妞 2020-09-22 配置Vue

# 配置优化

# 静态资源与index分离

通过配置静态资源的存放路径,得以缓解同一域名并发http请求的数量限制, 有效分流以及减少多余的cookie的发送

module.exports = {
    publicPath: process.env.NODE_ENV === 'development' ? '/' : process.env.NODE_ENV === 'staging' ? 'xxxbuild' : 'xxxtest',
}
1
2
3

# 缓存策略

将长时间不会改变的第三方类库或者静态资源设置为强缓存, 将max-age设置为一个足够久的时间, 改变的资源通过修改hash值保证获取到最新资源。vue-cli4中通过filenameHashing设置hash值,默认开启。

max-age需要后端配合设置

  • no-cache:即使没过期浏览器也要向服务器验证,不会从缓存读取。
  • no-store:即使服务器下发了缓存相关头,浏览器也会忽略任何和缓存相关的信息,发送请求不会携带相关头,直接去请求最新的数据。

https://www.cnblogs.com/lguow/p/10620940.html

# 合理使用sourceMap

sourceMap方便调试,但是线上非常耗时。可以通过productionSourceMap控制开启时间,在开发环境使用cheap-source-map辅助编码。

module.exports = {
    productionSourceMap: process.env.NODE_ENV === 'development'
    true: false,
    chainWebpack(config) {
        config
            // https://webpack.js.org/configuration/devtool/#development
            .when(process.env.NODE_ENV === 'development',
                config => config.devtool('cheap-source-map')
            )
    }
}
1
2
3
4
5
6
7
8
9
10
11

sourceMap 7 种模式:

模式 解释
eval 每个module会封装到 eval 里包裹起来执行,并且会在末尾追加注释 //@ sourceURL .
source-map 生成一个SourceMap文件.
hidden-source-map 和 source-map 一样,但不会在 bundle 末尾追加注释.
inline-source-map 生成一个 DataUrl 形式的 SourceMap 文件.
eval-source-map 每个module会通过eval()来执行,并且生成一个DataUrl形式的SourceMap.
cheap-source-map 生成一个没有列信息(column-mappings)的SourceMaps文件,不包含loader的 sourcemap(譬如 babel 的 sourcemap)
cheap-module-source-map 生成一个没有列信息(column-mappings)的SourceMaps文件,同时 loader 的 sourcemap 也被简化为只包含对应行的。

webpack 不仅支持这 7 种,而且它们还是可以任意组合上面的eval、inline、hidden关键字,就如文档所说,你可以设置 souremap 选项为 cheap-module-inline-source-map。 注2: 如果你的modules里面已经包含了SourceMaps,你需要用source-map-loader 来和合并生成一个新的 SourceMaps。

# 合理使用prefetch/preload/dns-prefetch

上面三个属性都可以做到预加载,但是后台管理中很少跳转其他资源,首屏渲染反而更加重要。 访问首页时,由于prefetch提前加载导致也加载了其他的chunk块,配置中移除可加快首屏渲染速度

module.exports = {
    chainWebpack: config => {
        // 移除prefetch插件,避免加载多余的资源
        config.plugins.delete('preload')
        config.plugins.delete('prefetch')
    }
}
1
2
3
4
5
6
7

dns-prefetch可以让浏览器提前对域名进行解析,减少DNS查找的开销, 如果你的静态资源和后端接口不是同一个服务器的话,可以将考虑你后端的域名放入link标签加入dns-prefetch属性。

# 分割代码

如ElementUI等第三方插件,随时有可能引入新的组件由于过大且不需要更新所以希望能存在缓存中,不需要设置hash值。

config
    .when(process.env.NODE_ENV !== 'development',
        config => {
            config
                .plugin('ScriptExtHtmlWebpackPlugin')
                .after('html')
                .use('script-ext-html-webpack-plugin', [{
                    // `runtime` must same as runtimeChunk name. default is `runtime`
                    inline: /runtime\..*\.js$/
                }])
                .end()
            config
                .optimization.splitChunks({
                    chunks: 'all',
                    cacheGroups: {
                        libs: {
                            name: 'chunk-libs',
                            test: /[\\/]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
                        }
                    }
                })
            config.optimization.runtimeChunk('single')
        }
    )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

splitChunks 常用参数

  • name 打包的 chunks 的名字
  • test 匹配到的模块奖杯打进这个缓存组
  • chunks 代码块类型 必须三选一: “initial”(初始化) | “all”(默认就是 all) | “async”(动态加载)默认 Webpack 4 只会对按需加载的代码做分割。如果我们需要配置初始加载的代码也加入到代码分割中,可以设置为 ‘all’
  • priority :缓存组打包的先后优先级,数值大的优先
  • minSize (默认是30000)形成一个新代码块最小的体积
  • minChunks (默认是1)在分割之前,这个代码块最小应该被引用的次数
  • maxInitialRequests (默认是3)一个入口最大的并行请求数
  • maxAsyncRequests(默认是5)按需加载时候最大的并行请求数
  • reuseExistingChunk 如果当前的 chunk 已被从 split 出来,那么将会直接复用这个 chunk 而不是重新创建一个
  • enforce 告诉 webpack 忽略 splitChunks.minSize, splitChunks.minChunks, splitChunks.maxAsyncRequests and splitChunks.maxInitialRequests,总是为这个缓存组创建 chunks

# 代码压缩去除console.log

module.exports = {

    configureWebpack: config => {

        if (process.env.NODE_ENV === 'production') {

            config.plugins.push(
                new TerserPlugin({
                    terserOptions: {
                        ecma: undefined,
                        warnings: false,
                        parse: {},
                        compress: {
                            drop_console: true,
                            drop_debugger: false,
                            pure_funcs: ['console.log'] // 移除console
                        }
                    }
                })
            )
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# Gzip

通常开启gzip压缩能够极大缩小传输资源的大小。服务端可直接压缩,前端压缩需要ngnix的配合。 安装compression-webpack-plugin让webpack插件,在打包的时输出.gz后缀的压缩文件,这是一种利用空间换时间的方法。

const CompressionPlugin = require('compression-webpack-plugin')
const productionGzipExtensions = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i
module.exports = {
    configureWebpack: {
        plugins: [new CompressionPlugin({
            algorithm: 'gzip',
            test: productionGzipExtensions,
            threshold: 10240, // 超过10k压缩
            minRatio: 0.8, // 只有压缩率小于这个值的资源才会被处理
            deleteOriginalAssets: false // 删除原文件
        })]
    },
}
1
2
3
4
5
6
7
8
9
10
11
12
13

附nginx配置代码:

    #开启和关闭gzip模式
    gzip on;
    #gizp压缩起点,文件大于1k才进行压缩
    gzip_min_length 1k;
    # gzip 压缩级别,1-9,数字越大压缩的越好,也越占用CPU时间
    gzip_comp_level 6;
    # 进行压缩的文件类型。
    gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript ;
    #nginx对于静态文件的处理模块,开启后会寻找以.gz结尾的文件,直接返回,不会占用cpu进行压缩,如果找不到则不进行压缩
    gzip_static on
    # 是否在http header中添加Vary: Accept-Encoding,建议开启
    gzip_vary on;
    # 设置gzip压缩针对的HTTP协议版本
    gzip_http_version 1.1;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# webpack-bundle-analyzer

webpack-bundle-analyzer是用来分析 Webpack 生成的包体组成并且以可视化的方式反馈给开发者的工具. 你可以来查看依赖关系. 然后再根据具体情况划分代码块,生成打包时模块划分的情况

npm run build --report
1

#

#

# 代码优化

# 简写目录

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

module.exports = {
    resolve: {
        alias: {
            '@': resolve('src'),
            'assets': path.resolve(__dirname, 'src/assets')
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 路由懒加载

使用路由懒加载,让url匹配到相应的路径时动态加载页面组件,首屏渲染速度加快。

const staticRoute = [{
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
}]
1
2
3
4
5

# keep-alive缓存页面

< template >
    <
    div id = "app" >
    <
    keep - alive >
    <
    router - view / >
    <
    /keep-alive>  < /
div > <
    /template>
1
2
3
4
5
6
7
8
9
10
11

# 使用v-show复用DOM

v-show首次渲染开销大,但是如果切换频繁的情况下,v-show比v-if更适合使用

# v-for 遍历避免同时使用 v-if

源码中v-for优先于v-if被解析,如果同时出现,每次渲染时斗魂执行循环再执行判断,浪费性能。

  • 外层嵌套template,在这层进行v-if判断
  • 判断出现在循环内部时,可以先通过计算属性过筛
<template>
  <ul>
    <li v-for="item in list" :key="item.id">
    {{ item.name }}
    </li> 
  </ul>
</template>
<script>
  export default {
    computed: {
      activeList: function () {
        return this.list.filter(function (item) {
          return item.enabled 
        })
      } 
    }
	}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 记得写Key

设置key可高效更新虚拟DOM,不设置key可能在列表更新时引起一些隐蔽bug。

  • 父组件中有一个从数组中删除一项的方法,原本期望通过这个方法能删除页面上的子组件。然而实际使用时,若删除数组的第k项,并不会将第k个problem组件删除,而是最后一个。原本绑定的ind是数组的下标,但删除数组的一项后,后续的下标又会更新,所以不能正确删除期望的组件。
  • 相同标签名元素过渡切换时需要使用key,否则vue只会更换内部属性而不会触发过渡效果

# 事件销毁解绑

Vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。

created() {
        this.timer = setInterval(this.refresh, 2000)
    },
    beforeDestroy() {
        clearInterval(this.timer)
    }
1
2
3
4
5
6

# 图片懒加载

可以使用vue-lazyload等插件对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域 内的图片先不做加载, 等到滚动到可视区域后再去加载。

< img v - lazy = "/static/img/1.png" >
1

# 插件按需引入

像element-ui这样的第三方组件库可以按需引入避免体积太大。

import Vue from 'vue';
import {
    Button,
    Select
} from 'element-ui';
Vue.use(Button) Vue.use(Select)
1
2
3
4
5
6

# 无状态组件可标记成函数式组件: <template functional>

用来定义那些没有响应数据,也不需要有任何生命周期的场景,它只接受一些props 来显示组件

# 批量导入filter等

import * as filters from '@/filter'

Object.keys(filters).forEach(k => Vue.filter(k, filters[k]))
1
2
3

# 合理组件化

组件拆分粒度并不是越小越好,可维护性和复用性才是拆分组件的出发点。可分为下面三部分:

  • 通用组件:基本功能可复用,如按钮等
  • 业务组件:完成具体业务,如轮播图等
  • 页面组件:页面间组件切换,如列表页和详情页

组件渲染会通过renderComponentRoot去生成组件的子树Vnode,再递归patch去处理这个子树Vnode。同样的div封装成组件之后会比直接渲染要多处理一次生成组件的子树vnode的过程,并且还要设置带副作用的渲染函数。

# 冻结纯展示数据

如果列表是纯粹的数据展示,不会有任何改变,就不需要做响应化

export default {
    data: () => ({
        list: []
    }),
    async created() {
        const list = await axios.get("/api/list");
        this.list = Object.freeze(list);
    }
};

//或者
var o = {
    a: 1
};
Object.defineProperty(o, "a", {
    configurable: false, // 不可配置
    writable: false //不可写
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

深冻结:

function completelyFreezeObj = (obj) => {
    if (Object.prototype.toString.call(obj)! = "[object Object]") {
        console.error("obj不是对象");
        return;
    }
    Object.freeze(obj);
    Object.keys(obj).forEach((key, i) => {
        if (Object.prototype.toString.call(obj[key]) == "[object Object]") {
            completelyFreezeObj(obj[key]);
        }
    });
};
1
2
3
4
5
6
7
8
9
10
11
12

# 变量本地化

和for循环中变量提取一样,let i = 0 不要写在表达式中而是要提取出来,操作数据是操作父节点,拼接完后再赋值。

# 待优化

# require.context

一个webpack的api, 通过执行require.context函数获取一个特定的上下文, 主要用来实现自动化导入模块, 在前端工程中, 如果遇到从一个文件夹引入很多模块的情况, 可以使用这个api, 它会遍历文件夹中的指定文件, 然后自动导入, 使得不需要每次显式的调用import导入模块。

https://juejin.im/post/6844903736209309703

# CDN

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

https://blog.csdn.net/u014231144/article/details/83791877

# Dllplugin

这种打包方式专门引用webpack官方的DllPlugin和DllReferencePlugin。DllPlugin会生成一个dll包的代码指纹manifest,管理额外的打包。而在项目生成的过程中,DllReferencePlugin会参考manifest的内容去打包。额外生成的js文件应该被放置在vue项目的文件当中的static文件夹底下,以便于代码部署。 开发过程中个人编写的源文件才会频繁变动,而一些库文件我们一般是不会去改动的。如果能把这些库文件提取出来,就能减少打包体积,加快编译速度。 参考PaicFE/vue-multi 中的配置文件webpack.dll.config.js的写法。 https://www.cnblogs.com/lifefriend/p/10479341.html

# SSR

如果项目比较大,首屏无论怎么做优化,都出现闪屏或者一阵黑屏的情况。可以考虑使用SSR(服务端渲染),vuejs官方文档提供next.js很好的服务端解决方案。但是局限性就是目前仅支持Koa、express等Nodejs的后台框架,需要webpack支持。