likes
comments
collection
share

通过这些case,我把项目LCP时间减少了1.5s

作者站长头像
站长
· 阅读数 11

您好,如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~

前言

最近在做公司几个项目性能优化,整理出一些比较有用且常见的case来分享一下。

A项目优化

白屏相关

DNS预连接、资源预解析

对于公共域名g.alicdn.cmon,DNS预请求:

<link rel="preconnect" href="//g.alicdn.com" crossorigin />
<link rel="dns-prefetch" href="//g.alicdn.com" />

对于一些资源,资源预加载:

<link rel="preload" href="https://g.alicdn.com/eleme-risk/chuangdao-pc/0.0.99/js/index.js" as="script" />
<link rel="preload" href="//g.alicdn.com/alilog/mlog/aplus_v2.js" as="script" />

结果:白屏时间减少400~600ms左右。

页面级路由懒加载

原本创道打包出来的JS文件只有一个bundle.js,涵盖了整个项目的业务代码,对于城市CM来说,可能访问最多的就是新增定向看和任务详情两个页面,所以对于首屏加载是不友好的,应该优化成访问哪个页面加载对应页面的资源,基于Ice2.0调研,将路由中的组件都转换为懒加载模式:

  1. routes.ts
import { lazy, IRouterConfig } from 'ice';
// ice不支持layout组件设置为懒加载
import Layout from '@/layouts/BasicLayout';

const Home = lazy(() => import(/* webpackChunkName: 'Home' */ '@/pages/Home'));
const NotFound = lazy(() => import(/* webpackChunkName: 'NotFound' */ '@/components/NotFound'));
const ManualDetect = lazy(() => import(/* webpackChunkName: 'ManualDetect' */ '@/pages/ManualDetect'));
const AddMission = lazy(() => import(/* webpackChunkName: 'addMission' */ '@/pages/ReconnaissanceMission/add-mission'));
const MissionDetail = lazy(
  () => import(/* webpackChunkName: 'missionDetail' */ '@/pages/ReconnaissanceMission/missionDetail'),
);
const NewMissionDetail = lazy(
  () => import(/* webpackChunkName: 'newMissionDetail' */ '@/pages/ReconnaissanceMission/newMissionDetail'),
);
const NoPermission = lazy(() => import(/* webpackChunkName: 'NoPermission' */ '@/pages/NoPermission'));
const Board = lazy(() => import(/* webpackChunkName: 'Board' */ '@/pages/Board'));
const BusinessInsight = lazy(() => import(/* webpackChunkName: 'BusinessInsight' */ '@/pages/BusinessInsight'));
const ChuangDaoInsight = lazy(() => import(/* webpackChunkName: 'ChuangDaoInsight' */ '@/pages/ChuangDaoInsight'));
const Report = lazy(() => import(/* webpackChunkName: 'Report' */ '@/pages/Report'));

const routes: IRouterConfig[] = [
  {
    path: '/',
    component: Layout,
    children: [
      {
        path: '/manualDetect',
        component: ManualDetect,
      },
      {
        path: '/addMission',
        component: AddMission,
      },
      {
        path: '/MissionDetail',
        component: MissionDetail,
      },
      {
        path: '/newMissionDetail',
        component: NewMissionDetail,
      },
      {
        path: '/',
        exact: true,
        component: Home,
      },
      {
        path: '/noPermission',
        exact: true,
        component: NoPermission,
      },
      {
        path: '/board',
        exact: true,
        component: Board,
      },
      {
        path: '/businessInsight',
        exact: true,
        component: BusinessInsight,
      },
      {
        path: '/chuangDaoInsight',
        exact: true,
        component: ChuangDaoInsight,
      },
      {
        path: '/report',
        exact: true,
        component: Report,
      },
      {
        component: NotFound,
      },
    ],
  },
];

export default routes;

2.build.json

{
	// ...
  "router": {
    "lazy": true
  }
}

线上效果:

首屏在A页面:

通过这些case,我把项目LCP时间减少了1.5s

只请求了对应A页面的代码,JS文件大小12.7KB,再进入到立即检查页面:

通过这些case,我把项目LCP时间减少了1.5s

继续请求了对应跳转新页面的代码,文件大小也是KB量级的,再看一下优化前的首屏请求情况,无论访问哪个页面,请求的资源是一样的。

通过这些case,我把项目LCP时间减少了1.5s

通过这些case,我把项目LCP时间减少了1.5s

结果:白屏时间整体降低,请求资源大小整体下降。

构建相关

优化本地热更新时间

创道的本地热更新时间比较慢,大约在8~9秒,基于ice运行时中间件在每次代码变更时加入缓存同时移除对node_module目录下的babel转换。

module.exports = ({ onGetWebpackConfig }) => {
  onGetWebpackConfig((config) => {
    config.module
      .rule('tsx')
      .test(/.jsx?|.tsx?$/)
      .exclude.add(/node_modules/)
      .end()
      .use('babel-loader')
      .tap((options) => {
        return {
          ...options,
          cacheDirectory: true,
        };
      });
  });
};

在build.json中注入该插件:

{
  // ...
  "plugins": [
    "@ali/build-plugin-faas",
    [
      "build-plugin-ignore-style",
      {
        "libraryName": "antd"
      }
    ],
    "@ali/build-plugin-ice-def",
    "./src/index.ts"
  ]
}

通过这些case,我把项目LCP时间减少了1.5s

结果:热更新时间降低到4秒左右,降低50%。

构建包大小优化

CDN资源替代项目依赖包

通过webpack可视化工具可以看到创道PC端的一些依赖包体积偏大,影响了页面渲染的时间:

通过这些case,我把项目LCP时间减少了1.5s

通过这些case,我把项目LCP时间减少了1.5s

从上图可以看到:在开发环境整个构建包体积达到了19.44MB,echarts、antv、moment这些包,体积都比较大,达到了MB量级,并且在项目中前两者使用频率很低,只有引用过一次,对于这种情况,考虑将依赖包转换为CDN引入的方式,原因如下:

  • 减少打包产物大小;
  • 减少白屏时间;
  • 版本固定,使用频率低,通过CDN单独引入还会有浏览器强缓存的效益;

解决方案:

通过webpack中externals,解绑对于node_modules中枚举包的编译,并且在项目index.html中从CDN引入所列举到的包。

{
	// ...
  "externals": {
    "echarts": "echarts",
    "moment": "moment"
  },
}

这里的key,value值分别对应npm中的包名和CDN引入后在window下的全局变量名,找包的CDN路径很简单,但是如何知道全局变量名是什么呢?

可以打开CDN链接,格式化代码,大概是这个样子的:

function(e, t) {
    "object" == typeof exports && "object" == typeof module ? //判断环境是否支持commonjs模块规范
    module.exports = t(require("vue")) :
    "function" == typeof define && define.amd ? //判断环境是否支持AMD模块规范
    define("ELEMENT", ["vue"], t) :
    "object" == typeof exports ? //判断环境是否支持CMD模块规范
    exports.ELEMENT = t(require("vue")) : 
    e.ELEMENT = t(e.Vue)
} ("undefined" != typeof self ? self: this,function(e){
    //省略...
});

只需要看一下立即执行函数向外暴露的变量名是什么即可。

代码分割

对于项目中多次引用到的包和公共模块,开启webpack代码分割模式,这部分代码写在之前定义的运行时中间件中:

module.exports = ({ onGetWebpackConfig }) => {
  onGetWebpackConfig((config) => {
    config.optimization.splitChunks({
      cacheGroups: {
        vendor: {
          priority: 1,
          test: /node_modules/,
          chunks: 'initial',
          minChunks: 1,
          minSize: 0,
          name: 'vendor',
          filename: 'vendor.js',
        },
        common: {
          chunks: 'initial',
          name: 'common',
          minSize: 100,
          minChunks: 3,
          filename: 'common.js',
        },
      },
    });
  });
};

抽离出来的vendor.js模块如图:

通过这些case,我把项目LCP时间减少了1.5s

结果:优化后的构建包体积为9.1MB,降低了50%以上大小。

目前对于创道H5做了如下优化内容:

B项目优化

白屏相关

HTML文件脚本加载改造

由于JS是单线程,脚本加载会直接阻塞页面渲染,因此对于一些直接放在HTML模板中并且优先级较低的JS文件,在创道H5中例如aplus埋点、vconsole判断加载包、exlog性能监控等与用户体感上无关的脚本文件直接异步加载解析即可:

<script defer>
  try {
    const isXuanYuan = /AliApp(EVE//i.test(navigator.userAgent);
    if (isXuanYuan) {
      document.documentElement.setAttribute('data-theme', 'xy')
    } else {
      document.documentElement.setAttribute('data-theme', 'default')
    }
  } catch (e) {

  }

</script>
<script defer>
  (function (w, d, s, q, i) {
    w[q] = w[q] || [];
    var f = d.getElementsByTagName(s)[0],
      j = d.createElement(s);
    j.async = true;
    j.id = 'beacon-aplus';
    j.setAttribute('exparams', 'clog=o&aplus&sidx=aplusSidx&ckx=aplusCkx');
    j.src = 'https://g.alicdn.com/alilog/mlog/aplus_wap.js';
    f.parentNode.insertBefore(j, f);
  })(window, document, 'script', 'aplus_queue');

</script>
<script defer>
  const domain = location.hostname;
  let env = 'prod';
  if (/.(((alibaba|taobao|tmall).net)|daily.elenet.me)$/.test(domain) || location.hostname === 'local.ele.me') {
    // 集团&饿了么 DAILY
    env = 'dev';
  } else if (/^(pre|ppe)-\w+./.test(domain)) {
    // 集团&饿了么 PRE
    env = 'pre';
  }
  console.log('env:', env)
  const debug = {
    dev: true,
    pre: false,
    prod: false,
  } [env];
  window._ex = {
    biz: 'a2fe9.26877649', // 配置spm或业务标识
    bizType: 'KOUBEI', // 固定入参,不要更改
    debug: debug, // 开启后会在console打印日志,但注意不会上报,仅用于调试
    enableOutsidePerformance: true, // 端外上报性能需要开启
    commonParams: {}, // 添加全局参数,也可以直接赋值到window.ExLog.commonParams = {}
    whiteScreen: true, // 是否开启白屏监控
  };
  // if (env !== 'prod') {
  //   eruda.init();
  // }
  if (env !== 'prod') {
    const vConsoleScript = document.createElement('script');
    vConsoleScript.src = 'https://cdn.bootcdn.net/ajax/libs/vConsole/3.9.1/vconsole.min.js';
    document.body.appendChild(vConsoleScript);
    vConsoleScript.onload = () => {
        var vConsole = new VConsole();
    }
  }
</script>
<script src="https://gw.alipayobjects.com/as/g/koubei-data-center/exlog/1.4.1/index.js" defer></script>

优化前:

通过这些case,我把项目LCP时间减少了1.5s

资源加载的并发性偏低,也直接影响到了云鼎接口的调用时机,平均在1400ms的时候才会调(走到useEffect),LCP平均为1300ms。

优化后:

通过这些case,我把项目LCP时间减少了1.5s

资源加载的并发度提高了很多,并且平均在1100ms的时候就会开始调云鼎,LCP平均为1000ms,提升了300ms。

DNS预请求、资源预解析

由于项目使用umi,开发创道本身umi plugin给head插入一些link标签从而进行优化:

/plugins/preloadPlugin.ts

import type { IApi } from 'umi';

export default (api: IApi) => {
  api.addHTMLLinks(() => {
    return [
      {
        href: '//shadow.elemecdn.com',
        rel: 'dns-prefetch',
      },
      {
        href: '//g.alicdn.com',
        rel: 'dns-prefetch',
      },
      {
        href: '//gw.alipayobjects.com',
        rel: 'dns-prefetch',
      },
      {
        href: '//render.alipay.com',
        rel: 'dns-prefetch',
      },
      {
        href: 'https://shadow.elemecdn.com/faas/chuangdao-h5-fe-gray/umi.c166c725.js',
        rel: 'preload',
        as: 'script',
      },
      {
        href: 'https://shadow.elemecdn.com/faas/chuangdao-h5-fe-gray/layouts__BasicLayout.e2bc9944.async.js',
        rel: 'preload',
        as: 'script',
      },
      {
        href: 'https://shadow.elemecdn.com/faas/chuangdao-h5-fe-gray/wrappers.b5ead63e.async.js',
        rel: 'preload',
        as: 'script',
      },
    ];
  });
};

.umirc.ts中加入该插件

import { defineConfig } from 'umi';

export default defineConfig({
  // ...
  plugins: [require.resolve('./src/plugins/preloadPlugin.ts')],
});

优化前:

FP/FCP与LCP跨度较大,js资源请求比较分散

通过这些case,我把项目LCP时间减少了1.5s

优化后:

LCP快了500ms左右,同时js资源请求并发度高了,重复利用起来了。

通过这些case,我把项目LCP时间减少了1.5s

结尾

本文记录了博主工作中实际优化到的一些实用case,优化是灵活的,需要根据自己的场景来定,对你有帮助那就最好不过啦。

如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~