性能优化

34 阅读6分钟

前端代码打包之后的生成的静态资源就要发布到静态服务器上,这时候就要做对这些静态资源做一些运维配置,其中,gzip和设置缓存是必不可少的。这两项是最直接影响到网站性能和用户体验的

gzip

服务器对文件进行gzip 压缩后,再进行传输,浏览器收到资源后再解压的过程。

优缺点

  • 对于 js、text、json、css 这种纯文本进行压缩,效果特别好,不用改变代码即可提升网站响应速度;
  • 压缩过程是需要花费 CPU 资源的,对大文件(图片、音乐等)进行压缩,不仅不能提升网站响应速度,还会增加服务器压力,让网站有明显的卡顿感。

使用

安装依赖:npm i --save-dev compression-webpack-plugin@5.0.1

在vue.config.js顶部引入依赖

const CompressionWebpackPlugin=require('compression-webpack-plugin')

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

// 代码压缩
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

// gzip压缩
const CompressionWebpackPlugin = require('compression-webpack-plugin')

// 是否为生产环境
const isProduction = process.env.NODE_ENV !== 'development'

// 本地环境是否需要使用cdn
const devNeedCdn = true

// cdn链接
const cdn = {
    // cdn:模块名称和模块作用域命名(对应window里面挂载的变量名称)
    externals: {
        vue: 'Vue',
        vuex: 'Vuex',
        'vue-router': 'VueRouter'
    },
    // cdn的css链接
    css: [],
    // cdn的js链接
    js: [
        'https://unpkg.com/vue@next',
        'https://cdn.staticfile.org/vuex/3.1.0/vuex.min.js',
        'https://cdn.staticfile.org/vue-router/3.0.3/vue-router.min.js'
    ]
}

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

        // ============注入cdn start============
        config.plugin('html').tap(args => {
            // 生产环境或本地需要cdn时,才注入cdn
            if (isProduction || devNeedCdn) args[0].cdn = cdn
            return args
        })
        // ============注入cdn start============
    },
    configureWebpack: config => {
        // 用cdn方式引入,则构建时要忽略相关资源
        if (isProduction || devNeedCdn) config.externals = cdn.externals

        // 生产环境相关配置
        if (isProduction) {
            // 代码压缩
            config.plugins.push(
                new UglifyJsPlugin({
                    uglifyOptions: {
                        //生产环境自动删除console
                        compress: {
                            warnings: false, // 若打包错误,则注释这行
                            drop_debugger: true,
                            drop_console: true,
                            pure_funcs: ['console.log']
                        }
                    },
                    sourceMap: false,
                    parallel: true
                })
            )

            // gzip压缩
            const productionGzipExtensions = ['html', 'js', 'css']
            config.plugins.push(
                new CompressionWebpackPlugin({
                    filename: '[path].gz[query]',
                    algorithm: 'gzip',
                    test: new RegExp(
                        '.(' + productionGzipExtensions.join('|') + ')$'
                    ),
                    threshold: 10240, // 只有大小大于该值的资源会被处理 10240
                    minRatio: 0.8, // 只有压缩率小于这个值的资源才会被处理
                    deleteOriginalAssets: false // 删除原文件
                })
            )

            // 公共代码抽离
            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'
                        }
                    }
                }
            }
        }
    }
}

配置nginx

server{
    listen 8080
    server_name localhost
    
    gzip on;
    gzip_min_length 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;
    gzip_vary on;
    gzip_disable "MSIE [1-6].";
    
    location /appShare {
        client_max_body_size 10m;
        root /home/test/webIndex/appShare;
        try_filtes $uri $uri/ /yourProduct/index.html;
        index index.htm index.html;
    }
}

查看是否已压缩

打开控制台,响应头中有 Content-Encoding: gzip ,表示已经开启

缓存

优缺点

  • 减少了不必要的数据传输,节省带宽
  • 减少服务器的负担,提升网站性能
  • 加快了客户端加载网页的速度
  • 用户体验友好
  • 资源如果有更改但是客户端不及时更新会造成用户获取信息滞后,如果老版本有bug的话,情况会更加糟糕

强缓存

简单粗暴,如果资源没过期,就取缓存,如果过期了,则请求服务器。 当设置了强缓存,且没有过期时,就会从本地读取数据(内存中或硬盘中)

在chrome浏览器中的控制台Network中size栏通常会有三种状态

  1. from memory cache: 资源在内存当中
  2. from disk cache:资源在硬盘中(一般是样式)
  3. 资源本身的大小(如:1.5k)

强缓存又分为Expires 和 Cache-Control, Expires几乎已经不用,主要讲解Cache-Control

Cache-Control

有以下几个属性

  • private: 仅浏览器可以缓存
  • public: 浏览器和代理服务器都可以缓存(对于private和public,前端可以认为一样,不用深究)
  • max-age=xxx 过期时间(重要)
  • no-cache 不进行强缓存(重要)
  • no-store 不强缓存,也不协商缓存,基本不用,缓存越多才越好呢

协商缓存

协商缓存利用Last-Modified , If-Modified-Since 和 ETag , If-None-Match来实现

触发条件:

  1. Cache-Control 的值为 no-cache (不强缓存)
  2. 或者 max-age 过期了 (强缓存,但总有过期的时候)

ETag和 Last-Modified 是在每个请求的响应头部中(response headers)

  • ETag:每个文件有一个,改动文件了就变了,可以看似md5
  • Last-Modified:文件的修改时间

在下次请求时

  • 如果名字变了,就在请求头(request headers)携带If-None-Match,
  • 修改时间变了,,就在请求头(request headers)携带携带If-modified-since

疑问?

你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag呢?

  • 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
  • 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
  • 某些服务器不能精确的得到文件的最后修改时间。

vue项目如何做缓存

index.html文件采用协商缓存,理由就是要用户每次请求index.html不拿浏览器缓存,直接请求服务器,这样就保证资源更新了,用户能马上访问到新资源,如果服务端返回304,这时候再拿浏览器的缓存的index.html

hash、chunkhash和contenthash三者的区别

  1. hash

如果都使用hash的话,所有文件的hash都是一样的,而且每次修改任何一个文件,所有文件名的hash至都将改变。

所以一旦修改了任何一个文件,整个项目的文件缓存都将失效。

output:{
     path:path.resolve(__dirname,'./dist'),
     publicPath: '/dist/',
     filename: '[name]-[hash].js'
 }

2. chunkhash

使只有被修改了的文件的文件名hash值修改

output:{
     path:path.resolve(__dirname,'./dist'),
     publicPath: '/dist/',
     filename: '[name]-[chunkhash].js'
 }
如果我一个js文件里面引入了css文件。这时要是我修改了js,但没修改css,
能否让css能够继续利用缓存呢?答案是可以! 
首先,我们使用Extract-text-webpack-plugin插件将css文件从js中分离出来。
{
    test: /.css$/,
    use: ExtractTextPlugin.extract({
        fallback: "style-loader",
        use: {
            loader:"css-loader",
            options:{
                minimize: true //css压缩
            }
        }

    })
},
然后设置css的plugin
new ExtractTextPlugin({
      filename: 'css/[name]-[chunkhash].css',
  }),

3. contenthash

对css使用了chunkhash之后,我们测试会发现,如果修改了js直接,css文件名的hash值确实没变,

但这时要是我们修改css文件的话,我们就会发现css文件名的hash值居然没变化,

这样就导致我们的非覆盖发布css文件失效了。所以这里需要注意就是css文件必须使用contenthash。

将上面的css插件配置改为如下:

new ExtractTextPlugin({
    filename: 'css/[name]-[contenthash].css',
}),

懒加载

图片懒加载

图片的加载是由src属性引发的,当对src属性赋值时,浏览器就会发送一个http请求图片资源。根据这个原理,我们可以使用HTML5的data-src属性来存储图片的路径,在需要加载图片的时候,将data-src中图片的路径赋值给src,这样就实现了图片的按需加载,即懒加载。

当图片出现在可视区域时,获取图片的真实地址并赋值给图片即可, 以下使用IntersectionObserver来实现

IntersectionObserver两个参数

  • callback是当被监听元素的可见性变化时,触发的回调函数
  • options是一个配置参数,可选,有默认的属性值
<div class="container">
  <img style="height:1080px;" data-src="./png1.png" />
  <img style="height:1080px;" data-src="./png2.png" />
  <img style="height:1080px;" data-src="./png3.png" />
  <img style="height:1080px;" data-src="./png4.png" />
  <img style="height:1080px;" data-src="./png5.png" />
  <img style="height:1080px;" data-src="./png6.png" />
</div>
<script>
  const imgs = document.querySelectorAll("img");
  const observe = new IntersectionObserver((observes) => {
    observes.forEach((ele) => {
      //如果isIntersecting为true,则说明元素进入可视化区域
      if (ele.isIntersecting) {
        //do something...
        ele.target.src = ele.target.dataset.src;
        observe.unobserve(ele.target); //取消监听
      }
    });
  });
  //创建observe后需要给定一个目标元素进行观察:
  imgs.forEach((el) => {
    observe.observe(el);
  });
</script>

路由懒加载

其实就是将引用方式写成函数调用的形式

import Vue from 'vue'
import Router from 'vue-router'
// import HelloWorld from '@/components/HelloWorld'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: ()=>import("@/components/HelloWorld")
      //  component:HelloWorld
    }
  ]
})

回流和重绘

回流:当元素尺寸、结构或者属性发生变化时,浏览器会重新元素的过程

重绘:元素的样式发生变化,同时不会影响其在文档流中的位置时

如何避免回流和重绘

  • 使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
  • 避免频繁操作DOM,可以创建一个文档片段documentFragment,在它上面应用所有DOM操作,最后一次性添加到文档中
  • 将元素先设置display:none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘
  • 将DOM的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写
浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。

防抖和节流

webpack优化

  • loader优化:减少文件搜索范围
module.exports = {
  module: {
    rules: [
      {
        // js 文件才使用 babel
        test: /.js$/,
        loader: 'babel-loader',
        // 只在 src 文件夹下查找
        include: [resolve('src')],
        // 不会去查找的路径
        exclude: /node_modules/,
        options: {
          cacheDirectory: true, // 开启babel编译缓存,下次只需要编译更改过的代码文件即可
          cacheCompression: false, // 缓存文件不要压缩,压缩需要时间 
      }
    ]
  }
}
  • appyPack: 开启多线程

受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,HappyPack 可以将 Loader 的同步执行转换为并行的

module: {
  loaders: [
    {
      test: /.js$/,
      include: [resolve('src')],
      exclude: /node_modules/,
      // id 后面的内容对应下面
      loader: 'happypack/loader?id=happybabel'
    }
  ]
},
plugins: [
  new HappyPack({
    id: 'happybabel',
    loaders: ['babel-loader?cacheDirectory'],
    // 开启 4 个线程
    threads: 4
  })
]
  • DllPlugin: 将特定的类库提前打包然后引入,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案
// 单独配置在一个文件中
const path = require('path')
const webpack = require('webpack')
module.exports = {
  entry: {
    // 想统一打包的类库
    vendor: ['vue']
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].dll.js',
    library: '[name]-[hash]'
  },
  plugins: [
    new webpack.DllPlugin({
      // name 必须和 output.library 一致
      name: '[name]-[hash]',
      // 该属性需要与 DllReferencePlugin 中一致
      context: __dirname,
      path: path.join(__dirname, 'dist', '[name]-manifest.json')
    }),
  ]
}
plugins: [
    new webpack.DllReferencePlugin({
      manifest: require('./dist/vendor-manifest.json')
    })
  ]

DllPlugin和DllReferencePlugin参考文档

  • Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动webpack时追加参数 --optimize-minimize 来实现