记前端项目首屏加载优化(打包篇)

记前端项目首屏加载优化(打包篇)

看了一下我司官网的webpack打包出来的大小情况,发现有很多可以优化的点,比如 lodash、moment.js、antd等等;
本文主要围绕webpack的打包优化,并根据业务情况适当的做减法。

优化前分析

优化前一定要有一个界面能记录目前的打包情况,推荐用webpack-bundle-analyzer这个包, 它可以看到打包后每个模块的大小,还能给出gizp压缩后的大小,在生产环境中加载的模块都是经过gzip压缩过的,可以作为真实访问的大小依据。
安装也很简单:

// cli
npm install --save-dev webpack-bundle-analyzer
 

注意生产环境(production)是代表线上真实的环境,所以analyzer要对生产环境的包进行分析的,所以我配置了一下本地打包生产环境的构建配置,在package.json加入下面的配置:

"scripts": {
    ...
    "local_production": "cross-env NODE_ENV=local_production npm run build"
}

然后在webpack配置里面判断process.env.NODE_ENV === 'local_production',构建production环境的构建并且加入analyzer分析生产环境打包出来的情况。

// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;


if(process.env.NODE_ENV === 'local_production') {
  webpack_config.plugins.push(
    new BundleAnalyzerPlugin(
      {
        analyzerMode: 'server',
        analyzerHost: '127.0.0.1',
        analyzerPort: 8889,
        reportFilename: 'report.html',
        defaultSizes: 'parsed',
        openAnalyzer: true,
        generateStatsFile: false,
        statsFilename: 'stats.json',
        statsOptions: null,
        logLevel: 'info'
      }
    )
  );
}

这里是我的项目用analyzer生成出来的包大小情况(打包前)

主要看index.xxxx.js,它包含了所有的公共依赖,我们要做的就是减少不必要的公共资源的体积,可以减少大量不必要的代码。

逐个击破

分析antd

从上面的可以看出来antd.less占了很大部分面积,因为我要在项目中自定义theme,但是官方的那套配置的形式来自定义theme只能修改变量,不能改组件,所以我先加载所有的antd.less再在后面接着加载一个theme.less用于修改主题变量和修改antd组件样式。

  • 可能是我当时搭项目的时候想太多了,由于是官网项目,所有的组件都是根据ui来自己写的,很少用到antd的组件,项目开发了几十个页面了也没有用到这种自定义组件的情况,所以其实可以不加载这个庞大的antd.less,然后antd按需加载是必须的。
  • 后来发现我项目中用到的antd组件只有两个(轮播和单选框),其实轮播是可以用react-slick替代的,而单选框更是可以自己实现的,所以大胆的直接把antd给移除掉了,用其他插件替代即可。

移除了antd之后index包小了三百多k,这还远远不够,接着看下面的优化点

优化lodash

lodash也是需要优化按需加载的方式的,推荐这篇教程Webpack按需打包Lodash的几种方式, 按照教程改进后,lodash 小了500多k。

优化moment

其实moment引进来的时候会带有很多语言包的,我们只用到了其中一个中文的包,所以其他语言包都可以去掉,

plugins: [
    new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /zh-cn/),
]

后来又发现项目中只用到了moment().format()这个方法,由于moment.js只有一个大的moment.js模块,没有按模块分开写,无法按需打包,那么其实我们可以自己实现个简易版的moment来替代moment.js,下面是我找到的实现简易版moment代码:

// 简易版moment代替moment.js
class Moment {
  private date:Date;
  constructor(arg = new Date().getTime()) {
    this.date = new Date(arg);
  }
  padStart(num) {
    num = String(num);
    if (num.length < 2) {
      return '0' + num;
    } else {
      return num;
    }
  }
  unix() {
    return Math.round(this.date.getTime() / 1000);
  }
  static unix(timestamp) {
    return new Moment(timestamp * 1000);
  }
  format(formatStr) {
      const date = this.date;
      const year = date.getFullYear();
      const month = date.getMonth() + 1;
      const day = date.getDate();
      const week = date.getDay();
      const hour = date.getHours();
      const minute = date.getMinutes();
      const second = date.getSeconds();
      const weeks = ['一', '二', '三', '四', '五', '六', '日'];

      return formatStr.replace(/Y{2,4}|M{1,2}|D{1,2}|d{1,4}|h{1,2}|m{1,2}|s{1,2}/g, (match) => {
          switch (match) {
          case 'YY':
              return String(year).slice(-2);
          case 'YYY':
          case 'YYYY':
              return String(year);
          case 'M':
              return String(month);
          case 'MM':
              return this.padStart(month);
          case 'D':
              return String(day);
          case 'DD':
              return this.padStart(day);
          case 'd':
              return String(week);
          case 'dd':
              return weeks[week];
          case 'ddd':
              return '周' + weeks[week];
          case 'dddd':
              return '星期' + weeks[week];
          case 'h':
              return String(hour);
          case 'hh':
              return this.padStart(hour);
          case 'm':
              return String(minute);
          case 'mm':
              return this.padStart(minute);
          case 's':
              return String(second);
          case 'ss':
              return this.padStart(second);
          default:
              return match;
          }
      });
  }
}

export const moment = (arg) => {
  return new Moment(arg);
};

这样就直接可以把moment.js 干掉了,包体积又小了不少。

下面是优化后的analyzer生成出来的包大小情况

包体从2.7M优化到了1.7M,gzip从297k减小到212k,访问虽然只是快了一点点,但在低网速环境下访问还是看得到区别的。

首屏加载视觉优化

接下来讲一个跟包大小无关又很重要的优化点,就是单页应用的第一个入口html,正常情况下入口html只是用来加载js包,等js加载完之后才渲染出相关界面出来,这个入口html本身没有内容展示,但它是整个网站的第一个请求,取到这个入口html之后才开始加载js,等到加载完js才开始渲染界面,这段时间是占网站整体加载时间最多的,如下图:

第一个请求只要128ms,直到加载完公共js渲染出界面需要1s左右,这时候如果入口index没内容的话那就是纯粹的白屏时间了,所以我们应该好好利用这个入口index.html,可以做一个骨架屏或者loading动画,能让用户在等白屏时间里能够有个界面能看到,停留时间会更长一些,也能让用户以为这个网站一下就刷出来看到东西的感觉。

对于这个入口index的利用,我是加入了顶部导航栏进去的,让用户可以第一眼看到导航栏知道有什么导航项,而且也是可以点进去的,而内容区对于不同的路径访问会有不同的界面,所以我就简单的弄个loading即可。

至此,这一版优化减少了加载的时间,同时合理利用了入口index作为loading页,提高用户体验。

总结

前端优化工作是一个长期且复杂的工作,有很多可以考虑的地方,可以根据网络环境、框架、用户群体、业务情况、代码结构等多个方面合理地安排选择优化方案,本文只是我对于现有公司官网的优化的一部分,在这里分享给大家,如果觉得有用就点个赞吧👍

posted @ 2018-09-05 14:25  lipten  阅读(4660)  评论(1编辑  收藏  举报