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

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

前言

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

A项目优化

白屏相关

DNS预连接、资源预解析

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

html 复制代码
<link rel="preconnect" href="//g.alicdn.com" crossorigin />
<link rel="dns-prefetch" href="//g.alicdn.com" />

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

html 复制代码
<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
typeScript 复制代码
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

typeScript 复制代码
{
	// ...
  "router": {
    "lazy": true
  }
}

线上效果:

首屏在A页面:

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

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

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

构建相关

优化本地热更新时间

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

typeScript 复制代码
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中注入该插件:

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

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

构建包大小优化

CDN资源替代项目依赖包

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

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

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

解决方案:

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

typeScript 复制代码
{
	// ...
  "externals": {
    "echarts": "echarts",
    "moment": "moment"
  },
}

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

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

typeScript 复制代码
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代码分割模式,这部分代码写在之前定义的运行时中间件中:

typeScript 复制代码
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模块如图:

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

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

B项目优化

白屏相关

HTML文件脚本加载改造

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

html 复制代码
<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>

优化前:

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

优化后:

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

DNS预请求、资源预解析

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

/plugins/preloadPlugin.ts

typeScript 复制代码
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中加入该插件

typeScript 复制代码
import { defineConfig } from 'umi';

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

优化前:

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

优化后:

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

结尾

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

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

相关推荐
辻戋18 小时前
从零实现React Scheduler调度器
前端·react.js·前端框架
徐同保18 小时前
使用yarn@4.6.0装包,项目是react+vite搭建的,项目无法启动,报错:
前端·react.js·前端框架
Qrun19 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp19 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.20 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl1 天前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫1 天前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友1 天前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理1 天前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻1 天前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js