likes
comments
collection
share

记一次真实场景的网页性能优化(umi3单页项目)

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

背景

项目是一个基于umi3的单页项目(也可以类比基于Webpack的单页项目),最终打包成静态文件后使用Nginx部署。想要优化的原因是部署到服务器后网页访问速度慢,首页打开时能感受到明显的白屏与卡顿。(不说了🙊,说就是公司经费有限)。本文记录了每一步优化的思考与操作流程,最终效果是首屏性能与交互体验得到了显著提升。

优化过程中有用到下面三个分析工具:

  • Lighthouse
  • Chrome开发者工具的Network
  • Webpack打包可视化工具

V1版本

先看看第一个版本的网页效果。v1使用的是umi默认的打包机制,打包后只生成一个js文件与一个css文件,在下面的网络图中可以看到。

记一次真实场景的网页性能优化(umi3单页项目)

图中标明了5个方框,可以看成是首页资源加载的5个阶段。分别是:

  1. index.html文件
  2. js文件与css文件
  3. 一个pages请求
  4. 一个blocks请求与一些图片资源
  5. 一些图片资源

不难看出来,这5个阶段是串行的,而这也是阻碍首屏性能的关键点。为什么会有这5个阶段呢?下面来一一解析。

第一步是html文件。一般单页项目的html就长下面这样,里面没什么内容,但会引入需要的css与js文件。

<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- 一些其他信息 -->
    <link rel="stylesheet" href="/umi.a42575e9.css" >
  </head>

  <body>
    <div id="root"></div>

    <script src="/umi.bc260ca8.js"></script>
  </body>
</html>

第二步就是加载html里的css与js文件了,它们包含了这个单页项目的所有代码。

后面的步骤就是自己代码里写的东西了。代码中的逻辑是先发出一个pages请求,得到结果后,再发一个blocks请求。可以看下面的示意图。一个page可以看成是一个导航,每个page下面有多个block,每个block就是一个富文本。

记一次真实场景的网页性能优化(umi3单页项目)

第五步,在得到blocks的结果后,请求block里的一些图片资源。现在读者应该可以理解为什么这里会有5部分串行的请求了。

V1到V2的优化流程

优化前我们要弄清楚“主要矛盾”,这里的主要矛盾就是网络请求慢,从一个900多KB的JS文件要加载1.74s就可以看出来。所以我们的主要目标就是要减少网络请求时间。

提取关键JS/CSS

意思就是我们先只加载首页需要的JS,CSS,其他不相关的先放一边,后面需要的时候再加载。

这里我采取的一个主要方案是:页面拆分+路由懒加载。路由懒加载大家都比较熟悉,但这里的页面拆分指什么呢?

前面有讲到,每个页面都是一些block富文本块,这些富文本块内容是可以通过管理员账号编辑的。而之前富文本块的展示与编辑都是写在一个组件里,代码里也没有区分用户端与管理端。但用户其实只需要看到展示内容,所以我们需要把编辑富文本的逻辑都从首屏迁移出去。相比于对编辑富文本需要的组件一个个懒加载,更简便的方法就是把编辑逻辑整个抽离到另一个页面里去,即分为一个用户页页面与一个管理页面。

在umi中,通过添加dynamicImport配置就可以开启路由懒加载。这里的loading是页面资源加载完成之前的展示内容。

dynamicImport: {
  loading: '@/components/loading'
}

开启路由懒加载后的打包结果如下。vendors文件中是不同页面的公共组件。

记一次真实场景的网页性能优化(umi3单页项目)

这一步优化后,umi.js从941KB到了574KB,并且管理端页面与登录页面都被拆了出来。

合并请求

V1版本中,请求pages与请求blocks是两个串行的请求。优化后,我们将其合并为了一个,即一次返回pages与需要的blocks。这样就减少了一次请求时间。

开启gzip压缩

使用gzip压缩的目的是减小文件大小从而缩短请求时间。在Nginx中添加如下配置:

# gzip
#开启gzip模式
gzip on;
#gizp压缩起点,文件大于1k才进行压缩
gzip_min_length 1k;
# gzip 压缩级别,1-9,数字越大压缩的越好,也越占用CPU时间
gzip_comp_level 6;
# 进行压缩的文件类型。
gzip_types
  application/javascript
  application/x-javascript
  application/xml
  text/xml
  text/plain
  text/javascript
  text/css;
# nginx对于静态文件的处理模块,开启后会寻找以.gz结尾的文件,直接返回,不会占用cpu进行压缩,如果找不到则不进行压缩
gzip_static on;
# 是否在http header中添加Vary: Accept-Encoding,建议开启
gzip_vary on;

同时,在umi中增加compression-webpack-plugin,用于在打包时生成gzip文件。这样就不需要浪费服务器资源来压缩了。

chainWebpack(config, { env }) {
  if (env === 'production') {
    config.plugin('compression-webpack-plugin').use(
      new CompressionPlugin({
        test: /.js$|.html$|.css$/, // 压缩js,html,css文件
        threshold: 10240, // 对超过10k的数据压缩
        deleteOriginalAssets: false, // 不删除源文件
      }),
    );
  }
}

压缩后,前面500多KB的JS文件就只有190KB了。

图片压缩&预加载

下面的展示图中有一张图片,也是这个网站的一个门面。由于这张图片占位比较大,V1版本中可以明显感觉到这张图片从无到有的加载过程。那有没有办法让这个过程变得无感呢?

记一次真实场景的网页性能优化(umi3单页项目)

这里的解决方法就是预加载。实现方式是在umi的html模版里添加一行:

<link rel="preload" href="/menmian.jpeg" as="image">

在使用图片前,我们可以先对它进行压缩。这里使用的是tinypng

缓存接口数据

可能大家平常听的都是缓存JS,CSS,图片这些静态资源,为啥要缓存接口数据呢?其实这里是跟业务性质相关的,并不是所有场景都适用。这里每次切换page都会请求对应的blocks内容,但blocks的内容并不要求那么高的实时性。最主要还是请求一个blocks接口竟然要1s!(不说了,说就是经费不够)。

于是,这里对接口数据做了缓存,这样用户再切换到相同的page时就不用重新请求了。实现方法也很简单,如下:

// 缓存请求富文本块内容
const blockCache = new Map();
export const requestPageBlocksWithCache = async (
  id,
) => {
  const cache = blockCache.get(id);
  if (cache) return Promise.resolve(cache);

  const data = await request(`/pages/${id}/blocks`);

  blockCache.set(id, data);

  return data;
};

体验优化

然后还有一些体验优化,处理的都是一些慢网速下的问题。包括:

  • 页面loading骨架屏

  • 接口请求loading

  • 修改后直接更新变量而不是重新请求整个列表

V2版本

到这,V2的优化也就差不多了。相信读者或多或少觉得应该还算做了点东西,效果应该会好点。然而。。。

记一次真实场景的网页性能优化(umi3单页项目)

每一步确实有每一步的效果,页面是懒加载了,图片是预加载了,接口请求是少了。可是咋还是这么多段请求呢!

原来是这样,先加载html,再加载umi,再加载layout,再加载userpage。这中间又阻塞了,猝不及防呀,这还比不上V1呢。但也不必着急,这不正是预加载可以做的嘛。layout,userpage这些资源都是我们需要的,所以可以一并和umi.js一起放在html里预加载,效果就和预加载图片一样。

然后,就到V3了。

V2到V3的优化流程

主要是解决V2中的资源阻塞问题。解决方案就是预加载。

这里通过一个umi插件来实现,主要思路是在构建完成后获取到所有构建文件名称,将需要preload的文件提取出来添加到html文件中(这里实现的比较简陋,没有处理一些可能的异常情况)。

import fs from 'fs';
import { parse } from 'node-html-parser';
import { join } from 'path';
import type { IApi } from 'umi';

const addPreloadLinksToHtml = (
  filePath: string,
  links: {
    rel: string;
    as: string;
    href: string;
  }[],
) => {
  const html = fs.readFileSync(filePath).toString();
  const root = parse(html);

  const head = root.querySelector('head');
  if (!head) return;

  links.forEach((link) => {
    head.innerHTML += `<link rel="${link.rel}" as="${link.as}" href="${link.href}">`;
  });

  fs.writeFileSync(filePath, root.innerHTML);
};

export default (api: IApi) => {
  const { paths } = api;

  api.describe({
    key: 'preload',
    config: {
      schema(joi) {
        return joi.array().items(joi.string());
      },
    },
    enableBy: api.EnableBy.config,
  });

  api.onBuildComplete(({ err, stats }) => {
    if (err) return;
    const assets = Object.keys(stats?.stats[0].compilation.assets ?? {});

    const preloadList = api.userConfig.preload as string[];
    const preloadAssets = assets.filter(
      (filename) =>
        preloadList.some((name) => filename.includes(name)) &&
        ['js', 'css'].includes(filename.substring(filename.lastIndexOf('.') + 1)),
    );

    const umiJS = assets.find((name) => name.startsWith('umi') && name.endsWith('.js'));
    if (umiJS) preloadAssets.unshift(umiJS); // 将 umijs 作为第一个预加载文件

    api.logger.info('Preload assets: ', preloadAssets);

    const preloadLinks = preloadAssets.map((filename) => {
      const suffix = filename.substring(filename.lastIndexOf('.') + 1);
      return {
        rel: 'preload',
        as: suffix === 'js' ? 'script' : 'style',
        href: filename,
      };
    });

    // add preload links to html
    const htmlFilePath = join(paths.absOutputPath ?? '', 'index.html');

    addPreloadLinksToHtml(htmlFilePath, preloadLinks);
  });
};

这里除了把需要预加载的页面资源文件添加进来之外,还加上了umi.js作为第一个预加载资源。原因是在http1,http1.1中对同一个域名的TCP连接数有限,如下图里方框中有三个请求最开始是灰色,其实就是在等待连接。所以将umi.js先进行预加载,避免体积大的文件请求等待。

记一次真实场景的网页性能优化(umi3单页项目)

V3版本

最后看V3版本的效果,基本上是符合预期了。一共只有三段请求,第一次是html,第二次是许多并行的页面资源请求以及预加载的图片,第三段就是接口请求block了。首屏体验也好了许多。

记一次真实场景的网页性能优化(umi3单页项目)

V4

是不是到这里优化就结束了呢?远远没有。肉眼能观察到的优化之处就有下面一些。

umi.js体积优化

从打包来看,umi.js这个文件还是有挺多可以优化的。比如定制化的polyfill减小core-js的引入体积,自己实现引入的组件库的一些组件。

记一次真实场景的网页性能优化(umi3单页项目)

开启http2

解决前面提到了并发连接数问题。

CDN

既然服务器网络这么慢,为什么不把静态资源放到CDN上呢?(别问,问就是经费有限)

服务端渲染SSR

在服务端请求了block数据后一次性返回html内容,减少请求时间。后面补充~

参考