记一次vue+nuxt性能优化

9,754 阅读9分钟

硬件环境: MacPro2018 i7+512G硬盘 16g内存+100M带宽 wifi连接

网站是一个电商网站,这次主要针对优化的是网站的首页

这次优化的网站是基于vue+nuxt开发的,nuxt是一个vue的SSR框架(它也可以生成一些纯静态的SPA),得益于nuxt本身框架的优化加上服务器端渲染,基于nuxt开发的网站性能已经能达到不错的水平,在正常的WiFi网络环境下网站的首页Lighthouse测试评分已经达到了满分。

Audits下主要的性能参数

Lighthouse是针对网站性能,SEO等很多指标进行测试的谷歌的开源软件,他可以模拟不同的网络环境对网站进行全面的指标收集,同时也会给你的网站提供一些优化的建议,你也可以在chrome->开发工具->Audits下使用他。类似的性能测试网站或者工具也有很多你可以在之前写的性能指标这篇文章中了解。由于网站还没有上线无法对真实的用户环境性能进行收集,这里就只针对开发环境性能工具进行一些环境模拟的数据收集。

在模拟slow4g的网络环境下网站首页的分数就不是那么理想了,说明这个页面还是有蛮大的优化空间的,那就先记录下他的性能指标数据,用作优化完之后进行比对

code coverage

还有一个重要的指标是页面的代码使用率(coverage),你也可以在chrome的开发工具->coverage测试这个指标,coverage标志着当前页面的js有多少是当前页面真正需要执行的,你可能只是使用了UI库的一个组件但是把整个UI库全部都打包了进来,你也通过tree sharking避免这种情况(现代浏览器javascript性能优化),如果你发现你的code coverage没有使用的代码比例比较高的话,你就要考虑是不是有可能你把不需要的代码都打包进来了。如果你使用webpack打包js,你可以使用Webpack Bundle Analyzer来分析你的js包文件。 这次优化的首页他的code coverage:

超过50%的code coverage也不是那么理想,好的code coverage应该能做到接近30%或者更低,当然不同的环境能做到的理想值肯定不一样,但是超过百分之50肯定是有优化的空间的.

减小js提高code coverage

现在浏览器中减少js的大小只能够最直接影响你当前页面性能的,只传输首屏需要的js文件,将其他的js的code split后分包加载或者懒加载能够非常直接的提高你的页面性能。

http2

优化前网站还是使用的http1.0协议,在使用http2对性能主要有几个优势

  • HTTP2可以用过一个TCP连接并行发送多个请求.这是HTTP2最重要的优势,因为现代浏览器对一个服务器限制了一定个数的TCP连接,在HTTP1中每一个请求都需要创建一个TCP请求,这意味着对一个server并行发送请求的个数就受浏览器对TCP连接数的个数限制。而HTTP2对一个server只需要建立一个TCP连接,一旦建立连接,client就可以通过这个连接并行的发送的请求,这不但可以减少client创建TCP连接消耗,同时服务器对一个client也只需要维护一个连接就可以了。
  • 流数据传输.在HTTP2中数据通过流来封装和传输。在HTTP1中数据是通过纯文本传输的。流传输的优势在于,他可以减少请求和响应的大小,而且让服务器和客户端可以更高效的解析。
  • 请求和响应头压缩,重用。

使用了HTTP2以后也同样来带了一些问题,HTTP2通过一个TCP连接发送请求,那他怎么知道哪个请求更重要呢?Resource loading, prioritization, HTTP/2,英语好的小伙伴可以了解一下(要翻墙),如果有机会的话,我也想针对这个话题写一篇博客。

在nuxt中js通过webpack+SplitChunksPlugin进行code split,SplitChunksPlugin是基于下面几个条件进行来判断包是不是需要拆分的

  • 新的包压缩后大小大于30kb就会被拆分
  • 新的包可以被共用或者是来自于node_modules文件夹的模块
  • 按需加载包的时候平行请求个数小于等于5个
  • 初始页面时所需的包平行请求小于等于3个

当你使用了HTTP2你可以自己配置平行请求的个数

config.optimization.splitChunks.maxInitialRequests = 20; // for HTTP2
config.optimization.splitChunks.maxAsyncRequests = 20; // for HTTP2

这意味着在更大的平行请求数的加持下,你的包可以更好的被拆分,多个包之间共享模块在平行请求个数之内可以被单独打包,一个js包内的js也可以更小,你的code coverage也会更好。

只加载首屏需要的文件

如果你使用的也是nuxt框架,你可以看一下你的nuxt.config.js文件,检查一下全局的head,css,plugins配置,这些都是全局加载的,也就是说不管你这个页面是不是真的需要某个文件,nuxt都会将这个文件打包到js包中或者直出一个一整个css文件。如果你真的需要在head中引入一个全局都需要使用的js文件,比如第三方的js文件,那你也可以考虑将这个script文件设置成async

在我们的项目中即使只有一个页面需要使用video.js,这个js文件还是在每个页面都引入了

css: [
    "normalize.css"
    "video.js/dist/video-js.css"
  ],
  plugins: [
     { src: "~/plugins/nuxt-video-player-plugin.js", ssr: false }
  ]

在优化了全局文件以后。可以关注一下我们的首页了,优化一下只有首屏需要的文件。首先针对不同的页面你需要分析你首屏的元素,找出最主要的element。

那在我们的网站首页最重要的就是轮播的第一张图片,他处在网站最中间的位置,占得位置也最大,是用户第一眼就会看到的地方.所以为了保证这第一张图片可以更早的被用户看到,轮播的第一个元素做了服务器端渲染,剩下的元素在第一次需要轮播的时候进行加载。

紧接着将除了首屏能看到的元素以外的"猜你喜欢模块"和"产品楼层"全部进行了懒加载

components: {
    FavoritePane: LazyComponent({
      component: () => import("@/components/index/FavoritePane.vue")
    }),
    FloorProvider: LazyComponent({
      component: () => import("@/components/index/FloorProvider.vue")
    })
  }
  
///////////////////////////////////LazyComponent关键代码段/////////////////////////////////////////////  
function gen({ component, option }) {
  let resolveComponent;
  return () => ({
    component: new Promise(resolve => {
      resolveComponent = resolve;
    }),

    loading: {
      mounted() {
        this.el = this.$el;
        this.checkInView();
        this.initListener();
      },
      render(h) {
        return h(loading, {
          props: {
            height: "300px"
          }
        });
      },
      props: {
        preLoad: {
          type: Number,
          default: option.preLoad || 0.9
        }
      },
      data() {
        return {
          el: null,
          rect: {}
        };
      },
      methods: {
        initListener() {
          on(this.el, this.checkInView);
        },
        getRect() {
          this.rect = this.$el.getBoundingClientRect();
        },
        checkInView() {
          this.getRect();
          if (
            inBrowser &&
            (this.rect.top < window.innerHeight * this.preLoad &&
              this.rect.bottom > 0) &&
            (this.rect.left < window.innerWidth * this.preLoad &&
              this.rect.right > 0)
          ) {
            this.load();
            this.destroy();
          }
        },
        load() {
          component().then(resolveComponent);
        },
        destroy() {
          off(this.el, this.checkInView);
        }
      }
    }
  });
}  
  
  

LazyComponent这里是一个异步组件,vue的异步组件可以设置loading的选项,在服务器端渲染的时候我们只输出这个loading,将位置占住,那么后续需要渲染异步组件的时候,就可以减少relayout所带来的开销。 将组件都懒加载了以后,原本用于图片懒加载的库也可以不在使用,这里我们的js就又小了一些。

将全局的js进行整理,并且将除首屏以外的组件都懒加载了以后我们的js从原本的670减少到了362,减少了百分之46,code coverage从56%减少到了46%

long task

long task会影响TTI,或者造成动画延迟,响应延迟,所以我们需要找出long task,拆分task,设置优先级去执行task。 在开发环境通过performance配合Timings和Main,找到长时间占用主线程的task,并且分析他是由哪些方法组成的。尝试将他拆分后,按优先级去执行他。

下面这个方法可以用于将拆分的组件先后执行,将一个long task 拆分为两个小的task

<ProductSwpier v-if="defer(1)"/>
<ProductInfo v-if="defer(2)" />

/////////////////////////////defer.js//////////////////////////////

export default function(count = 5) {
  return {
    data() {
      return {
        displayPriority: 0
      };
    },
    mounted() {
      this.countDisplayPriority();
    },

    methods: {
      countDisplayPriority() {
        const step = () => {
          if (this.displayPriority < count) {
            this.displayPriority++;
            requestAnimationFrame(step);
          }
        };
        requestAnimationFrame(step);
      },

      defer(priority) {
        return this.displayPriority >= priority;
      }
    }
  };
}

tips

async await

  async asyncData() {
    let banner = (await getBannerItemList()).data;

    let mediaObjSrc = (await getVideoEntrance()).data;
    return { banner, mediaObjSrc };
  }

上面的方法因为异步控制,两个异步任务即使是没有依赖关系,任然会一个个的执行,asyncData中没有依赖关系的异步任务用promise.all优化

  async asyncData() {
    let [banner,mediaObjSrc] = await Promise.all([getBannerItemList(),getVideoEntrance()])
    return { banner, mediaObjSrc };
  }

异步请求的数据用v-if优化

        <el-carousel class="carousel" height="500px">
          <el-carousel-item v-for="(item,index) in banner" :key="index">
           
          </el-carousel-item>
        </el-carousel>

即使banner数据任然没有返回el-carousel会先进行一轮 init->render->patch 但是因为没有数据patch的结果是空的。

        <el-carousel class="carousel" v-if="banner" height="500px">
          <el-carousel-item v-for="(item,index) in banner" :key="index">
           
          </el-carousel-item>
        </el-carousel>

用v-if进行优化以后只有在数据返回之后才会进行el-carousel-iteminit-render-patch

总结

上面介绍的是一些编程时候需要注意的小细节,以及一些优化的思路。但是优化是永远没有终点的,对于一个团队来说难的是一个注重性能优化的氛围或者文化。在很多团队或者项目中常常会发生,刚刚优化完的代码在进行了一段时间的迭代或者团队开发之后,性能又越来越不理想,正所谓小团队靠宣传,大团队靠工具。持续迭代中code review和注重性能的团队文化才是网站在持续迭代中保持较高性能的秘诀。