• 作者:老汪软件技巧
  • 发表时间:2024-09-14 00:01
  • 浏览量:

背景

去年接手一个比较老的前端项目,使用react16.13开发,webpack4.44.2打包,spa的项目。上线后业务侧反馈页面速度慢。

项目存在的问题

公司的项目都是使用sentry做监控,所以想着根据监控去排查下,再去看优化哪些链路。通过排查,项目存在的问题如下:

js体积大(组件库、html2canvas、mobx、),用户在移动设备上打开,大都是4g网络,带宽有限,加载时间长。页面内容依赖2个接口串行调用(下文中简称接口A、接口B),js加载完成后,还需要等待接口调用完成,才能展示出内容。js、css、图片等静态资源没有使用cdn,通过监控发现,ip为南方地区的用户请求js、css、图片时,时间会比较长页面内容非常多,有大概10屏的内容。使用react去渲染的时间也比较长。优化方式

找到了存在的问题,就针对这几点去进行优化。

SSR重构

要减少首屏时间,最有效果的就是将项目重构为ssr,但在实际工作中,将一个项目重构为ssr项目,成本还是比较大的,最后也是因为成本过大的原因,放弃了重构。

减少单个js文件的大小

减少单个js文件的体积,首先想到的就是webpack分包,首先通过webpack-bundle-analyzer进行打包依赖分析。这里有两点

通过下面的分包配置,我将单个js文件的体积都控制在了100kb以下。

// 以下配置webpack版本为4.44.2
optimization: {
        concatenateModules: true, //启动作用域提升
        splitChunks: {
            maxInitialRequests: 100,
            maxAsyncRequests: 100,
            cacheGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    chunks: 'initial',
                    name: 'vendors',
                    priority: 1,
                },
                common: {
                    name: 'common',
                    minChunks: 3,
                    priority: 2,
                    reuseExistingChunk: true
                },
                echarts: {
                    test: /[\\/]node_modules[\\/]echarts/,
                    chunks: 'all',
                    name: 'echarts',
                    priority: 3
                },
                sentry: {
                    test: /sentry/,
                    chunks: 'all',
                    name: 'sentry',
                    priority: 3
                },
                zrender: {
                    test: /[\\/]node_modules[\\/]zrender/,
                    chunks: 'all',
                    name: 'zrender',
                    priority: 3
                },
                antd: {
                    test: /[\\/]node_modules[\\/]antd-mobile/,
                    chunks: 'all',
                    name: 'antd-mobile',
                    priority: 3
                },
                reactSpring: {
                    test: /[\\/]node_modules[\\/]@react-spring/,
                    chunks: 'all',
                    name: 'react-spring',
                    priority: 3
                },
                mobx: {
                    test: /[\\/]node_modules[\\/]mobx/,
                    chunks: 'all',
                    name: 'mobx',
                    priority: 3
                },
                cssVarsPonyfill: {
                    test: /[\\/]node_modules[\\/]css-vars-ponyfill/,
                    chunks: 'all',
                    name: 'css-vars-ponyfill',
                    priority: 3
                },
            },
        }
    },

这两个配置主要是为了限制请求的并发数量,因为http1.1在同一个域名下有并发请求数量限制(6个)在分包后实际上请求的数量是变多的,因为项目开启了http2的支持,所以这个地方设置为了一个较大的数。

动态加载依赖动态加载

项目中实际有很多依赖,在首屏加载时是用不到的,这个时候可以使用import来进行动态引入。

比如点击按钮将页面生成图片进行下载,使用了html2canvas。

import(
    /* webpackChunkName: "html2canvas", webpackPrefetch: true */
    'html2canvas'
).then(async ({default: html2canvas}) => {
    ...
    }, 'image/jpeg');
}).catch(() => {
    Toast.show('生成报告图片失败,请点击重试');
});

注意此处的webpackPrefetch配置,此配置是在浏览器空闲时预先获取html2canvas,如果不加此配置,那么在点击按钮的时候再去加载html2canvas,用户会感到明显的延迟,那带来的就是用户体验上的问题了。

通过此配置,可以将html2canvas从打包的vendors中抽离出来。

注意我上面的配置,chunks设置为了initial,只会把静态引入的依赖打入到vendors.js中

vendors: {
    test: /[\\/]node_modules[\\/]/,
    chunks: 'initial',
    name: 'vendors',
    priority: 1,
},

组件动态加载

react中组件的动态加载是使用的lazy方法。我在项目中的实际应用是将echarts绘制的图片进行了动态加载。

const EchartsVigourAssignPie = lazy(() => import(/* webpackChunkName: "echartsVigourAssignPie" */'./echartsVigourAssignPie'));
{/* echarts扇形图 */}
<Suspense fallback={<Loading />}>
    <EchartsVigourAssignPie
        dataList={energyDistributionData}
        onFinished={() => onEchartsRenderFinished(EEchartsRenderFinishedType.energyDistributeEcharts)}
    />
Suspense>

使用动态加载的组件时,一定要搭配组件来进行使用,否则react是不知道这个组件在动态加载完成之前如何进行展示的。

这里我将使用echarts进行绘制的组件,进行了动态加载,所以在首屏渲染时,是不需要加载echarts的js依赖的,而是等到组件加载时,才需要引入echarts的依赖。

echarts的按需加载

这里还要提的一点是,在使用echarts的时候,是一定要进行echarts的按需加载的,如果使用import * as echarts from 'echarts'; 进行引入,会将整个echarts的包引入进来,体积会非常的大。具体引入方式可以参考echarts官网的教程:echarts按需引入

其他静态资源的体积压缩

除了js文件的压缩,项目中经常用到的静态资源还有html、css、图片、字体

html

html文件相比于其他类型的文件,压缩带来的提升不是很明显(因为一般的html文件本来就已经非常小了),只需要用html-webpack-plugin来进行常规压缩即可

new HtmlWebpackPlugin({
    template: indexPath,
    filename: 'index.html',
    inject: true,
    scriptLoading: 'defer',
    minify: {
        collapseWhitespace: isProduction,
        removeComments: isProduction,
        minifyJS: true, //压缩html文件中的js代码
        minifyCSS: true, //压缩html文件中的css代码
        minifyURLs: true //压缩html文件中的url链接
    },
})

css (对此方面研究不太多,后续进行补充)压缩插件css按需加载(主要是在开发前端组件库时需要进行css按需加载)图片

1. 图片格式的选择

常见的图片格式有svg、jpg、png、webp,下面给出google给出的如何选择图片的建议

存在项目问题怎么写_存在项目问题及建议_

看到这张图时,可能会产生1个疑问:

Q:既然svg的体积更小,还是矢量图,为什么不全部的图片都使用svg?

A:svg格式的图片并不一定比其他格式的图片更小。图标和一些小的icon,svg的体积确实更小,但是对于一些复杂的图片,svg的格式实际上是比其他格式更大的。 在开发时,我们从设计稿网站上去选择不同格式的图片进行下载,会发现对于一些负杂的图片,svg格式的大小,是远远大于png格式的3倍图的,所以实际开发过程中,要根据图片实际的大小去选择

webp格式的兼容性:

在使用webp格式的图片时,一定要判断当前浏览器是否支持webp格式,如果在不支持的浏览器中使用webp,那就显示不出来导致元素空白了

2. 懒加载

懒加载是最常见的一种优化手段,此处不做赘述

3. 渐进式图片(jpg)

对于某些比较大的jpg图片,还可以考虑使用渐进式图片(一种图片格式)

字体

1. font-display

一般我们定义和使用自定义字体的方式:

@font-face {
    font-family: "fangzheng_mid";
    src: url(~@/assets/font/fangzheng-mid-text.woff2);
    font-style: normal;
    font-display: swap;
}

其中font-display是比较重要的一个属性。具体可以看MDN中关于此属性的介绍 font-display 。这里只给出结论,一般可以使用font-display: swap;进行优化

2. 字体抽离 + unicode-range

当我们项目引入了一个字体包,字体包中包含所有字符,但实际上项目中并没有用到全部的字符。这个时候就需要对字体包进行抽离,只保留其中我们用到的字符,从而达到缩小字体包体积的目的。

可以使用fonttools工具对字体进行抽离,具体使用方法如下:

pip install fonttools

比如抽离abcdef:pyftsubset input-font-file.ttf --text="abcdef"

pyftsubset input-font-file.ttf --unicodes=U+0020-007F

在实际开发中,一般有两种情况:

1.只对某几个特殊的文字使用特殊字体展示

这种场景相对简单,只需要使用上面的命令,将特定的字体抽离出来作为一个字体包即可

2.不能确定有哪些字符需要使用字体

实际上在不考虑国际化的情况下,我们用到的字符也就是由三部分组成

同时,@font-face中有1个属性:unicode-range,可以指定当前字体的编码范围。当浏览器使用的字符符合当前的编码范围时,才会加载对应的字体文件。

//fangzheng_mid基本中文汉字
@font-face {
    font-family: "fangzheng_mid";
    src: url(~@/assets/font/fangzheng-mid-text.woff2);
    font-style: normal;
    font-display: swap;
    unicode-range: U+4E00-9FFF;
}
//fangzheng_mid基本拉丁字符
@font-face {
    font-family: "fangzheng_mid";
    src: url(~@/assets/font/fangzheng-mid-number.woff2);
    font-style: normal;
    font-display: swap;
    unicode-range: U+0020-007F;
}
.note {
    font-family: "fangzheng_mid", sans-serif;
}

若上代码所示,我使用fonttools工具将fangzheng字体包中的基本中文汉字和基本拉丁字符分别抽离出来,作为两个字体包,同时我使用font-face将这两个字体包设置为:

同一个名字fangzheng_mid。不同的编码范围

当我类名为note的元素文本内容为中文汉字时,浏览器只会加载fangzheng-mid-text.woff2,文本内容为数字时,只会加载fangzheng-mid-number.woff2,文本内容为汉字+数字时,两个文件都会加载。

假设我的文本内容是接口中返回的,不能确定有哪些内容,就可以利用此机制,让浏览器去自动选择加载哪个文件,从而做到减小字体包体积的目的。

在使用时,还要注意两个问题,

上面说的三个字符范围,只能包括常见的字体,某些生僻字是不包括在内的,如果有则需要将生僻字单独处理在使用fonttools,还有很多参数,这里我贴出我使用chatgpt生成的命令。其中各个参数的具体意义可以参考fonttools文档

pyftsubset ./fangzheng-mid.ttf --output-file=fangzheng-mid-number.woff2 --flavor=woff2 --unicodes=U+0020-007F --layout-features=' *' --glyph-names --symbol-cmap --legacy-cmap --notdef-glyph --notdef-outline --name-IDs='* '

todo:

预加载接口调用时机提前页面内容渲染优先级的控制


上一条查看详情 +JUC从实战到源码:中断机制与API实现
下一条 查看详情 +没有了