前端性能优化的思考过程

下面将从3个角度出发, 特别强调,本文不区分客户端渲染 还是 服务端渲染,因为每个业务的支持度不同,且需要根据互动营销所使用的技术栈来合理安排 客户端渲染还是服务端渲染,本人不盲目为服务端渲染带节奏。本文将从宏观的几个优化方向出发,不涉及具体到优先的细节(比如优化if else 或 超长列表的滚动优化)

一、为什么要做性能优化

如题,提高页面性能=提高用户体验=提高用户留存率和使用率=应用ROI高=公司业绩好团队绩效高=财富自由提高0.00000000001%.

1.1 如何检测性能指标?

在前端性能检测方面,有多种方式和工具。Chrome 的 Performance、开源的 Lighthouse、原生 Performance API,以及各类官方库和插件都能用于性能检测。像 Lighthouse、web-vitals 及相关浏览器插件,可获取 FCP、LCP、FID、CLS 等关键性能指标,这些指标反映了页面的加载、交互和稳定性情况。

在性能优化前,要参考历史数据和行业优秀标准,结合业务实际确定合理的性能标准。明确的标准能为优化工作指明方向,助力提升前端性能。

1.2 性能检测集成到CI中

在软件开发领域,性能问题一直是影响产品质量与用户体验的关键因素。以开源数据库 MongoDB 的使用为例,常见的 N+1 性能问题颇具代表性。假设业务需求是获取 N 条数据项,理想情况下,应通过一条高效的查询语句一次性返回所有数据,实现最佳性能。例如,在一个电商系统中查询用户及其对应的订单信息,若编码人员对 MongoDB 客户端 API 不够熟悉,可能会写出如下代码:

javascript 复制代码
// 错误示例
const users = await User.find();
for (const user of users) {
    await Order.find({ userId: user.id }); 
}

这段代码会导致每查询一个用户,就单独执行一次订单查询,即执行了 N + 1 次数据库查询请求,而非最优的 1 次查询。当 N 值较大时,如在一个拥有大量用户数据的系统中,这会显著增加数据库负载,拖慢系统响应速度,严重影响软件性能。

微基准性能测试集成

javascript 复制代码
pipeline {
    agent {
        node {
            label 'performance-test-node' 
            customWorkspace '/perf-test-workspace' 
        }
    }
    stages {
        stage('Micro Benchmark Test') {
            steps {
                // 执行微基准测试的命令或脚本
            }
        }
    }
}

宏基准测试涵盖范围更广,可进一步细分为全系统端到端的性能测试和组件 / 服务级的性能测试。

对于全系统端到端的性能测试,被测系统往往包含众多组件与服务,且这些组件和服务可能依赖多个代码仓。例如,一个大型电商平台系统,涉及用户管理、商品展示、订单处理、支付等多个组件,每个组件可能由不同团队开发并维护在不同代码仓中。若将系统级性能测试简单挂接到某一个具体代码仓提交触发的流水线上,显然并不合理,因为一个代码仓的变动可能无法全面反映整个系统的性能状况。针对这类性能测试,更合适的做法是定义独立的流水线。由于它与具体代码仓独立,可利用定时器触发机制定期触发性能测试流水线,并生成详细的性能测试报告。许多云服务化的性能测试工具,如阿里云的 PTS,都提供了方便的定时触发功能。即便使用单机版性能测试工具,也可通过巧妙设计流水线来实现类似的定时触发能力。

在组件 / 服务级的性能测试集成方面,将系统级性能测试拆分成组件 / 服务级进行测试具有诸多优势,其中之一便是更易于将其集成到 Pipeline 中。当把组件 / 服务级基线的性能测试集成到 Pipeline 后,其工作流大致如下:首先在原来的 Deploy 阶段,将组件或服务实例部署到预先设定好系统资源与规格(如确定的 CPU 核数、内存大小等)的被测运行环境中。完成部署后,新增一个自动化性能测试阶段(Performance Test),在此阶段执行对被测系统的自动化性能测试,并生成准确的性能测试报告。

核心变化与挑战 应对方法
测试执行频率大幅提高 优化测试脚本与环境配置,采用高效的测试框架和工具,减少测试执行时间。
对测试稳定性要求更高 增加测试用例的健壮性检查,在测试前对环境进行全面检查与初始化,确保每次测试环境一致。
性能测试与代码提交脱节问题 建立紧密的关联机制,在代码提交时自动触发性能测试,并将测试结果与代码版本关联记录。

通过将性能测试深度集成到 Pipeline 中,研发团队能够在每次代码提交时实时监测软件性能变化,尽早发现并解决性能劣化问题,避免因性能问题导致的软件产品质量下滑和研发成本增加。

groovy 复制代码
pipeline {
    agent {
        kubernetes {
            label 'perf-test'
            yamlFile 'k8s-perf-test.yaml' 
        }
    }
    stages {
        stage('Performance Test') {
            steps {
                // 在k8s集群中执行性能测试的命令或脚本
            }
        }
    }
}

1.3 检测渲染密度

在评估应用程序性能时,检测渲染密度是衡量用户体验的重要一环,尤其是对于首屏时间的计算。基于图片特征点比对的首屏时间计算方案,利用自动化工程手段,在真机环境中启动 APP 加载页面。通过录制 APP 加载页面过程的视频,对视频进行逐帧分析。其核心原理为计算每帧图片中的特征点数量,特征点即图像中具有独特性质的点,诸如角点、边缘点等。例如,在一个包含复杂图形和文字的页面中,图片的边缘、文字的边角等都可作为特征点。随着页面逐渐加载完成,特征点数量会呈现出一定的变化趋势。通过对这些特征点数量变化的分析,能够精准计算出合理的首屏时间。

1.4 检测视频分析

视频分析在性能检测中同样发挥着关键作用。通过对应用程序运行过程中的视频记录进行分析,可获取多维度的性能信息。例如,检测页面元素的加载顺序与速度,观察动画效果是否流畅,以及评估不同操作下页面的响应时间等。 对于页面元素加载,利用图像识别技术识别视频帧中的特定元素,通过记录元素首次出现的时间点来确定其加载时间。 在评估动画流畅性方面,可计算相邻帧之间的差异程度,若差异过大则表明动画可能存在卡顿。对于页面响应时间,可通过检测用户操作(如点击按钮)在视频中的时间点,以及操作后页面变化的时间点,计算两者之间的时间差来获取响应时间。 在实际应用中,可借助专业的视频分析工具或开发自定义的分析脚本实现上述功能,为性能优化提供有力的数据支持。

二、如何检测性能指标

2.1 如何优化性能

性能优化手段与工具介绍:

如果你是一个非常喜欢敲代码的程序员,那么你可能会对界面化的性能测试工具没有什么好感,毕竟这种工具是通过界面的配置能力来生成测试代码的,所以并没有代码实现灵活和丰富,影响程序员们对于代码掌控力的执着追求。在性能测试工具领域,传统的界面化工具往往因其依赖配置生成测试代码,在灵活性与功能丰富度上难以满足追求极致的开发者需求。然而,当下的技术演进带来了新的突破,像 Locust、k6 这类代码化性能测试工具应运而生,彻底改变了这一局面。

以 Locust 为例,它赋予开发者运用 Python 语言编写性能测试用例的能力,如同编写业务逻辑代码般自然流畅。通过 Python 强大的表达能力,我们能够构建出高度定制化的性能测试场景。例如,在测试接口时,发送携带 cookies 与 body 消息体的 POST 请求是常见需求,以下是一个实际测试用例片段:

js 复制代码
// test_submit_case.py
from locust import HttpLocust, TaskSet, task
class WebsiteTasks(TaskSet):
    @task
    def add_entry(self):
        body ={"field_1":"asdf"}
        cookies = dict(_session='V1hjaWV4Ujd3WndDWWZBa0ZiTjdvd2tlV2p3NmtkUzA0RTM2c213SUs3TXpDQlhqT1BoRzFKUVBwZEhXbThPMjZGdUU3bUpPYjRTNEx5RFBxaEJaUEFYVUtFZHhMdUFDd1o3SldScjFzbkc1TGpqV2xnRjdhSXRaUEIvYi95WHd1WlZZU3BiQ0VUM0U1M1BDVFNzLzFBPT0tLVB2bUkyMVgrQkJKK0VsNWI5bndzTFE9PQ%3D%3D--109ab671b0bbc1fd56af4b87a1fcea3d61faeedd', path='/', domain='localhost',  Expires='Tue, 19 Jan 2038 03:14:07 GMT')
        self.client.post("/test/api", json=body,cookies=cookies)

class WebsiteUser(HttpLocust):
    task_set = WebsiteTasks
    min_wait = 5000
    max_wait = 15000

完成上述代码编写后,只需简单操作,就能在 Locust 提供的界面中便捷地启动性能测试,并直观查看测试结果,轻松洞察系统性能表现。

k6 同样是一款极具特色的代码化性能测试工具。它不仅支持通过代码编写测试用例,还别具一格地提供了基于界面自动生成性能测试代码的功能,为开发者带来更多便利。以下是一段使用 Node.js 编写的 k6 性能测试用例,主要用于测试页面的 GET 操作:

js 复制代码
//test_get.js
import http from 'k6/http';
import { sleep } from 'k6';
export default function () {
  http.get('http://test.k6.io');
  sleep(1);
}

当你完成 k6 的安装后,在命令行中输入 k6 run testget.js,即可快速运行这个性能测试用例。测试结束后,系统会生成详细的测试结果,方便你深入进行性能分析。值得一提的是,k6 还是一款云服务化的性能测试工具。注册登录 k6 Cloud 后,你能直接在云端平台上,通过编写脚本或者使用界面化操作,轻松创建性能测试用例,随时随地开展性能测试工作。

在性能测试工具的生态中,还有一些工具虽功能并非十分完备,但在特定场景下却能发挥重要作用。比如 Postman,它能够针对某个 REST 接口临时进行性能测试。在性能测试用例的开发和调试阶段,Postman 可作为得力助手,帮助你快速验证接口性能。另外,Chrome 调试器也是性能测试的好帮手,借助它,你能够快速获取 Node.js 接口调用的 REST 请求代码,为编写性能测试用例提供便利。

为了全方位提升性能测试的效果与效率,我们不妨将性能测试工作前置,打破性能测试团队与特性开发团队之间的隔阂。让性能测试无缝融入整个研发流程,成为研发团队内部常态化、协作化的一项核心活动,从而确保在产品开发的各个阶段,性能问题都能得到及时关注与有效解决。

2.1.1 代码压缩
  • 选择适配框架与工具库:在不同业务场景下,应合理选用框架或工具库。例如,在移动端开发中,Zepto 相比 jQuery 更为轻量;在构建轻量级前端应用时,Preact 可作为 React 的替代方案;TweenLite 能满足大多数动画需求,可替代功能更为复杂的 TweenMax。
  • 按需引用 :以 lodash 库为例,若仅需merge功能,应直接引用lodash.merge,避免引入整个 lodash 库,从而减少代码体积。
  • 实现简易工具库 :以日期处理为例,起初使用 moment 库时,通过在 webpack 中配置webpack.IgnorePlugin(/^./locale$/, /moment$/),成功将打包体积从 600 多 k 降至 160 多 k。但考虑到仅使用简单时间函数,后改用 dayjs 库,打包体积仅为 6k。此外,对于一些常用且逻辑简单的 utils 功能,如检测当前设备是平板还是手机,可参考腾讯文档的检测方案自行实现,避免引入第三方库。
  • 借助工具分析包体 :使用webpack - bundle - analyzer插件,可直观分析包体结构,明确各模块所占体积,有助于针对性地优化代码。"
2.1.2 图⽚压缩
  • 图片格式选择 :在图片格式选择上,通常spine > svga > lottie > 序列帧 > apng。不同格式各有优劣,需根据具体场景选用。
  • WebP 格式:WebP 具有更优的图像数据压缩算法,能实现更小的图片体积与无肉眼差异的图像质量,同时支持无损和有损压缩模式、Alpha 透明及动画特性。在动画图像场景下,将 GIF 转换为 WebP,文件大小可减少一半。然而,WebP 存在兼容性问题,在 Safari 等浏览器中无法正常显示。可通过以下代码检测浏览器是否支持 WebP:
js 复制代码
const isSupportWebp = (nature = 'lossy') => {
    const strategies = Object.assign(Object.create(null), {
        'lossy': 'UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA',  //有损
        'lossless': 'UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==',  // 无损
        'alpha': 'UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAwAAAARBxAR/Q9ERP8DAABWUDggGAAAABQBAJ0BKgEAAQAAAP4AAA3AAP7mtQAAAA==',  // 透明
        'animation': 'UklGRlIAAABXRUJQVlA4WAoAAAASAAAAAAAAAAAAQU5JTQYAAAD/////AABBTk1GJgAAAAAAAAAAAAAAAAAAAGQAAABWUDhMDQAAAC8AAAAQBxAREYiI/gcA',  // 动图
    })
    return new Promise((resolve, reject) => {
        const img = new Image()
        img.onload = () => (img.width > 0 && img.height > 0) && resolve(true)
        img.onerror = () => resolve(false)
        img.src = `data:image/webp;base64,${strategies[nature]}`
    })
}

isSupportWebp().then(bool => {
    // true 表示支持,false 表示不支持
})
  • HEIC 格式:从 iOS 11 起,iPhone 7 及后续设备采用 HEIC 格式存储视频和图片,该格式占用空间小且画面质量高。但在非 Apple 设备上,HEIC 格式图片可能无法正常查看。可借助 Apple 的 Photos 框架识别 HEIC 图片:

前段时间在工作中接到反馈,一些用户会上传格式为 HEIC 的图片,造成运营人员无法在非 Apple 设备上查看图片的问题。 原因在于从 iOS 11 起,iPhone 7 及之后出产的设备,系统采用了新的格式来存储视频和图片。 因此,我们需要将 HEIC 格式的图片转换为非 Apple 设备上可以查看的格式(包括但不限于 JPG)。

首先,需要将 HEIC 格式的图片识别出来。

借助 Apple 提供的 Photos 框架中的方法,可以完成对 HEIC 图片的识别。

js 复制代码
import Photos

var isHEIC = false
let resources = PHAssetResource.assetResources(for: asset)
for resource in resources {
    let uti = resource.uniformTypeIdentifier
    if uti == "public.heic" || uti == "public.heif" {
        isHEIC = true
        break
    }
}

注意:我们需要获取到图片的 Asset 信息,才能判断图片是否为 HEIC 格式。 依然使用 Photos 框架中的方法,完成对 HEIC 图片的转换。

js 复制代码
import Photos

PHImageManager.default().requestImageData(for: asset, options: nil) { (imageData, dataUTI, orientation, info) in
    if isHEIC {  // 如果是 HEIC 图片
        guard let imageData = imageData,
            let ciImage = CIImage(data: imageData),
            let colorSpace = ciImage.colorSpace else { return }
        let context = CIContext()
        guard let jpgData = context.jpegRepresentation(of: ciImage, colorSpace: colorSpace, options: [:]),
            let jpgImage = UIImage(data: jpgData) else { return }
            // 将原图片替换为 jpgImage
    } else {  // 如果不是 HEIC 图片
        // 继续使用原图片
    }
}

依照以上方法,我们就可以识别 HEIC 图片,并将 HEIC 图片转换为 JPG 格式。

非IOS设备话,借助第三方工具比如 heic2any (地址:github.com/alexcorvi/h...

js 复制代码
import heic2any from 'heic2any';

const fileReader = new FileReader();
const fileReaderBuffer = new FileReader();

const file  // 图片文件

// 加载图片
function loadImg(img) {
    return new Promise((resolve, reject) => {
        img.onload = (e) => {
            resolve(e.target.result);
        };
        img.onerror = (e) => {
            reject(e);
        };
    });
}

// 压缩图片
fileReader.onload = async (e) => {
    const base64 = e.target.result;
    const img = new Image();
    let imgData = null;
    img.src = base64;
    img.filename = file.name;
    try {
        await loadImg(img);
        imgData = await compressImg(img);
        resolve(imgData);
    } catch (error) {
        console.error(error);
        resolve({
            file: null,
            url: ''
        });
    }
};

// 读取是否是heic格式图片
fileReaderBuffer.onload = async () => {
    const type = getFileType(fileReaderBuffer);
    if (type === 'unknown') {
        console.error('unknown image type');
        resolve({
            file: null,
            url: ''
        });
        return;
    }
    if (type.includes('/heic')) {
        heic2any({ blob: file, toType: 'image/jpeg' }).then((blob) => {
            fileReader.readAsDataURL(blob);
        }).catch(() => {
            resolve({
                file: null,
                url: ''
            });
        });
        return;
    }
    fileReader.readAsDataURL(file);
};

fileReaderBuffer.readAsArrayBuffer(file);
  • AVIF 格式:国外程序员 Daniel Aleksandersen 的测试表明,在相同 DSSIM 情况下,AVIF 格式图片体积比 WebP 更小。例如,3 秒品牌动画,GIF 体积 1.2MB,AVIF 体积 180KB,压缩比达 6.6:1。AVIF 和 WebP 都不支持渐进式图像解码,但 AVIF 的 alpha 通道支持 256 级半透明,在一定程度上减轻了对渐进式解码的需求。
测试素材 GIF 体积 AVIF 体积 压缩比
3 秒品牌动画 1.2MB 180KB 6.6:1
动态图表 950KB 150KB 6.3:1
透明背景动画 1.1MB 200KB 5.5:1

原文链接:search.ctrl.blog/?q=AVIF

结论是:AVIF 和 WebP 都不支持渐进式图像解码;一种为连接性差的人提供更好用户体验的技术。然而,AVIF 的文件大小相当小,同时 AVIF 的 alpha 通道支持 256 级半透明,有助于减轻对渐进式解码的需求。

p3-juejin.byteimg.com/tos-cn-i-k3...

图片格式深度对比
格式 压缩比 浏览器支持 透明度 动画 渐进加载
AVIF 1:15 现代浏览器 平滑 支持 支持
WebP 1:10 全平台 半透明 支持 支持
APNG 1:5 部分浏览器 半透明 支持 支持
GIF 1:3 全平台 硬边缘 支持 不支持
图片格式选择决策树
  • ① CDN响应式图⽚

专门为图片做优化的,通常包含缩放、格式转换等

可以把它看成是一个 API,通过传入尺寸、质量、格式等参数,获取对应的图片内容

使用场景

将更新频率较低的图片可以直接放在服务器上,既能降低访问延时,又可以适用于多种不同的场景

2.1.3 资源压缩

21 是否使用了 Brotli 或 Zopfli 压缩 22 图片是否被正确优化 23 Web Font 是否被正确优化

三、如何优化性能

行业标杆数据

指标 行业优秀值 达标值
LCP <2s <4s
FID <100ms <300ms
CLS <0.1 <0.2
首屏渲染 <1.5s <3s

3.1 加载优化

在网络请求优化领域,一个常见的误区是认为 HTTP2 一定比 HTTP1.1 快。实际上,HTTP2 在性能提升方面虽然成果显著,但并非在所有场景下都能绝对超越 HTTP1.1。

HTTP1.1 的性能瓶颈与 HTTP2 的改进

HTTP1.1 存在三个主要的效率制约因素:

  1. TCP 慢启动:在建立 TCP 连接初期,发送方会以较小的拥塞窗口发送数据,逐步探测网络状况以增加发送速率,这在一定程度上延迟了数据的快速传输。

  2. 多条 TCP 连接竞争带宽:为了并行传输多个资源,浏览器会对同一域名建立多个 TCP 连接。但这些连接会竞争有限的网络带宽,导致整体传输效率无法达到最优。

  3. 队头阻塞:在 HTTP1.1 中,请求和响应是按顺序进行处理的。如果一个请求在传输过程中出现延迟或阻塞,后续的请求都需要等待,即使它们之间并没有依赖关系。

HTTP2 引入了多路复用机制来应对这些问题。它在协议栈中新增了二进制分帧层,将数据分割成更小的帧进行传输。这一创新不仅实现了多路复用,还衍生出了其他关键特性:

  1. 请求优先级:客户端可以为不同的请求设置优先级,服务器根据优先级合理分配带宽,确保重要资源(如关键的 CSS 和 JavaScript 文件)优先传输,提升页面的加载速度和用户体验。

  2. 服务器推送:服务器能够主动将客户端可能需要的资源(如 HTML 页面引用的 CSS 和 JavaScript 文件)推送给客户端,减少了客户端额外的请求次数,缩短了页面首次加载时间。

  3. 头部压缩:HTTP2 采用 HPACK 算法对请求头和响应头进行压缩,有效减少了头部数据的传输量。考虑到浏览器在发送请求时,大部分数据是请求头,这一压缩机制显著提升了传输效率。例如,一个包含约 100 个资源的页面,若将请求头数据压缩至原来的 20%,传输效率将得到大幅提升。

尽管 HTTP2 解决了 HTTP1.1 中的队头阻塞问题,但由于它仍然基于 TCP 协议,TCP 数据包级别的队头阻塞问题依然存在。当网络出现丢包时,TCP 需要等待丢失的数据包重传,这会导致整个连接的传输速度降低。特别是在移动网络环境中,IP 地址切换频繁,TCP 连接需要重新建立,经历 "握手" 和 "慢启动" 过程,之前积累的 HPACK 字典也会丢失,进一步造成带宽浪费和延迟增加。而且,HTTP2 对一个域名只建立一个连接,一旦这个连接出现故障,整个网站的访问体验将受到严重影响。相比之下,HTTP1.1 虽然传输效率较低,但由于它会对一个域名建立 6 - 8 个连接,在部分连接出现问题时,其他连接仍能继续工作,对网站整体访问的影响相对较小。

HTTP2 下传统优化策略的转变

在 HTTP2 环境下,一些在 HTTP1.1 中常用的优化手段,如精灵图(Spriting)、资源内联(inlining)和域名分片(Sharding),可能会产生反效果:

  1. 精灵图:在 HTTP1.1 中,精灵图通过将多个小图片合并为一个大文件,减少了 HTTP 请求次数,从而提升性能。但在 HTTP2 中,多路复用已经大大提高了请求并发能力。此时使用精灵图,会使文件体积增大,不仅传输时间变长,而且在缓存方面也变得不利。因为只要精灵图中的部分图片更新,整个文件都需要重新下载,缓存失效的代价较高。
  2. 资源内联:资源内联是将一个资源嵌入到另一个资源中,如在 HTML 文件中嵌入 base64 编码的图片,目的是减少 HTTP1.1 下的请求次数。然而,在 HTTP2 中,这种方式破坏了多路复用和优先级策略。同时,内联资源无法独立缓存,当 HTML 文件更新时,即使内联的资源未变,也会随 HTML 一起重新加载。相比之下,非内联资源可以更好地利用缓存,特别是在资源本身更新频率较低的情况下。
  3. 域名分片:在 HTTP1.1 时代,由于浏览器对同一域名下的并发连接数有限制,域名分片通过将资源分散到多个域名下,突破了这一限制,提高了资源加载的并行度。但在 HTTP2 中,多路复用技术已经解决了连接数限制的问题。此时使用域名分片,不仅无法显著提升性能,还可能因为增加了 DNS 解析等额外开销,以及多个连接之间的资源竞争,限制了 HTTP2 多路复用的优势发挥。而且,HTTP2 的单个连接能够高效利用带宽,过多的连接反而可能导致管理成本增加和性能不稳定。
3.1.1 如何分析DNS解析耗时
  • Chrome - DevTools - NetWork
  • WebPageTest等
3.1.2 请求合并

JS/CSS合并 图⽚合并 数据请求合并

3.1.3 快照
  • 数据快照
  • html快照
  • canvas快照

3.2 逻辑并⾏

  • 分批加载
  • 数据预请求
  • 预渲染

分批加载

  • 数据预请求 首先在一开始还是先明确下这里所提及到的"预请求"的概念和常规的 http 的 options 请求有所区别,这篇文章所涉及到的预请求的概念都是在页面切换时候的页面请求的提请发送,跳转进入新页面后能够快速的获取到服务端的数据。 在APP的webview和小程序都可以预请求数据,

  • 分批加载:在页面资源加载过程中,采用分批加载策略可有效控制资源加载量,避免一次性加载过多资源导致卡顿。例如,对于长列表数据,可按照一定数量或特定规则分批加载数据,逐步呈现给用户,提升用户体验。首先在一开始还是先明确下这里所提及到的"预请求"的概念和常规的 http 的 options 请求有所区别,这篇文章所涉及到的预请求的概念都是在页面切换时候的页面请求的提请发送,跳转进入新页面后能够快速的获取到服务端的数据。

  • 数据预请求:数据预请求是在页面切换前预先发送请求,与页面切换异步进行。在 APP 的 webview 和小程序中均可实现。通过封装 fetch 请求方法,设置请求缓存,在页面跳转前发起缓存请求,跳转后从缓存中获取请求结果。例如,可将请求结果缓存到 Map 结构对象中,在再次请求时判断是否有缓存且缓存未过期,若有则直接使用缓存结果。同时,可参考 LRU 算法对缓存进行优化,提高缓存命中率。此外,可在路由配置中实现自动化预请求,减少业务方重复编写预请求逻辑。

  • 预渲染 :通过prerender - spa - plugin实现预渲染,可生成静态页面,提高页面渲染速度并利于 SEO。构建阶段,该插件会为每个需要预渲染的路由生成对应的 html 文件,文件中已包含部分内容。需注意,若资源与代码一同打包发布,构建时 CDN 资源可能尚未存在,此时需设置静态资源代理。相关配置如下:

js 复制代码
const PrerenderSPAPlugin = require('prerender-spa-plugin');
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;
const path = require('path');

module.exports = {
    plugins: [new PrerenderSPAPlugin({
        staticDir: path.join(__dirname, 'dist'),
        routes: [ '/' ], // 需要预渲染的路由,
        renderer: new Renderer({
            headless: true, // 开启无头浏览器
            renderAfterDocumentEvent: 'render-event', // 渲染的事件,只有触发了这个事件,插件才会开始爬取html
          }),
    })]
}
  • 合理安排优先级 把你所有的静态资源(JavaScript,图片,字体,第三方脚本,尺寸大的模块)列成一个表,然后把它们按优先级分成三组:基本核心功能(老浏览器也能浏览的核心内容)、增强体验效果(为现代浏览器设计的强大功能和丰富体验)、附加功能(不一定需要并且可以惰性加载的资源,比如字体、轮播脚本、视频播放器、分享按钮等)。

  • 缓存机制 基于HTTP的缓存分为强缓存和协商缓存。 强缓存就是当浏览器判断出本地缓存未过期时,直接取本地缓存,无需发起请求,此时的状态为200 from cache,在HTTP1.1版本后通过头部的cache-control max-age属性值规定的过期时长来判断缓存是否失效,这比之前使用expires过期时间更准确并且安全。 协商缓存则需要浏览器发起HTTP请求,来判断浏览器本地缓存的文件是否改变。

  • 为了避免用户每次访问网站都得请求文件,我们可以通过添加 Expires 或 max-age 来控制这一行为。Expires 设置了一个时间,只要在这个时间之前,浏览器都不会请求文件,而是直接使用缓存。而 max-age 是一个相对时间,建议使用 max-age 代替 Expires 。 不过这样会产生一个问题,当文件更新了怎么办?怎么通知浏览器重新请求文件? 可以通过更新页面中引用的资源链接地址,让浏览器主动放弃缓存,加载新资源。 具体做法是把资源地址 URL 的修改与文件内容关联起来,也就是说,只有文件内容变化,才会导致相应 URL 的变更,从而实现文件级别的精确缓存控制。什么东西与文件内容相关呢?我们会很自然的联想到利用数据摘要要算法对文件求摘要信息,摘要信息与文件内容一一对应,就有了一种可以精确到单个文件粒度的缓存控制依据了。

3.3 互动引擎优化

  • 渐进式渲染

    • 骨骼动画因为是 碎片拼凑,体积较序列帧要小很多,序列帧 在每帧都是一个完整的实体;而骨骼需要每个skin都加载完才会渲染。
  • 对象创建优化

    • 游戏对象创建和资源加载并⾏
    • 某⼀资源加载成功⽴即实例化
    • 实例化后⽴即渲染
  • 架构优化

    • Eva互动引擎采⽤了ECS架构,在ECS架构中有个重要的优势,组件的属性会按照顺序放在CPU缓存中,且CPU缓存的数据传输 速率⾮常快。

3.4 宿主容器优化

作者:满帮大前端 地址:juejin.cn/post/712633...

在资源方面权衡各种维护便利性,以及加载性能后,我们项目做了离线包,预加载等一些手段后,首页的打开速度可以控制在700-1000ms左右,在我们想进一步像200ms内打开页面迈进的时候,我们想到预加载页面以及公共库,预执行公共逻辑。最终效果如下

热门活动页面打开的效果图 (测试机型 小米6 普通机器)

一个是微前端方式打开(除了页面过渡,以及数据loading之外,页面基本150ms就打开) 一个是不使用微前端方式(明显能加载过程中的白屏,以及进度条,在页面缓存过后打开时间也差不多在1.2s左右)

技术方案 初步想法,让App提供一个webview预加载执行一个web项目,该web项目有运行一个有前端项目的所有依赖库。然后该项目分析访问的url,分析需要加载的业务页面逻辑js,然后加载js并创建渲染页面。 由于已经提前执行了公共逻辑。后续的业务页面加载执行其实非常快。 按这个想法我们需要进行web端项目和native项目进行改造

web端改造 上面是改造之前3个前端项目打包后的产物,包含html入口文件,公共的资源库,以及每个页面的js逻辑代码。如果一个公司里面按照公司的标准规范统一创建的项目,那么大多项目引入的公共资源库在每个项目里面都是一样的。 既然每个项目依赖的公共资源都一样,那我们是否可以想个办法把公共的东西当作一个项目,这个项目根据url的信息,动态加载对应url页面的js来运行,最终渲染页面,按这个想法我们调整了一下架构如下图。

可以看到我们把公共的依赖资源抽离成一个单独的项目,暂且叫做 微前端框架 。通过 static.ymm56.com/microweb 可以进行访问 。社区项目的某个页面可以通过如下链接进行访问 static.ymm56.com/microweb/#/... 可以看到我们把具体项目的路由信息放到hash后面(你完全可以定义自己的规则不使用hash)。 微前端框架通过获取hash值,分析出是social项目的D页面。然后通过动态加载D页面的逻辑js。执行并创建页面,最终渲染。 经过上面的调整,我们统一了所有前端项目的公共库,集中到了一个项目里面进行维护。这一步统一后我们就好去提前加载微前端框架项目并执行这些公共逻辑了。要知道基础框架初始化这个过程占了项目的90%时间,这个时间节省下来了,那200ms打开就不是问题了。 接下来需要app启动后预先加载一个webview。这个webview会直接加载微前端框架项目,当app被告知要打开一个h5页面。判断端该h5页面是否支持微前端,支持的话,就给预加载的这个微前端框架发一个消息告诉他加载这个页面,并让这个预加载的webview显示。 整个流程用户感知到的时间只有加载页面的js,直接执行。体验上是非常快的。 有想法了。接下来就是找客户端兄弟配合做细节完善了。走找客户端兄弟去。

客户端要做的事不复杂,直接上图,主要关注的问题如下

如何区分一个链接是微前端的,还是非微前端的(可以自行通过url域名等信息定好规则) 非微前端的项目还是走老的流程直接打开页面,微前端的项目则是给预加载好的webview发消息,并显示出来。 如何判断一个微前端框架已经加载好了,因为微前端项目最终需要接受容器的消息去加载一个页面的js,如果发消息之前微前端的容器还没加载好,那么收到这个消息也是没用的,所以一定需要微前端基础框架加载好后通知客户端,容器已经准备好了。 如果加载一个微前端页面的时候,容器还没初始化好。那就直接打开该页面。不用走微前端的形式打开 可以自行做队列控制最大初始化微前端容器的数量,来提供微前端的命中率。

app启动就初始化一个微前端容器,容器的html加载完,并且js逻辑执行完后会告诉客户端该微前端容器可用了。这个时候如果app接收到打开一个微前端页面,那么客户端会进过一系列逻辑判断后,给微前端容器发送打开的url信息,并把该微前端容器显示出来。 该微前端框架项目收到客户端的url后,解析url里面的hash,判断是打开什么项目的什么页面,然后在配置里面去加载该页面的js,加载完后执行,并渲染页面,当一个微前端容器使用后又接着初始化一个微前端容器,方便下次使用。

页面打开速度快 公共库和资源统一管理 业务项目标准化,能提供集中管控

所有项目都在微前端框架上运行,对框架的稳定性要求极高 升级和迭代微前端基础项目风险高

题外:优化过程什么时候结束

性能优化工作并非无休无止的工程,确定其结束节点至关重要,而这在很大程度上取决于投资回报率(ROI)的评估。从商业视角出发,老板们尤为关注投入资源进行性能优化所带来的实际效益。因此,在启动性能优化项目时,向老板清晰阐述该项工作的价值极为关键。

在向老板汇报时,要着重突出性能优化工作的高效性与高回报。例如,强调通过优化,能在相对较短的时间内实现用户体验的显著提升。需明确,没有可观 ROI 的性能优化任务,往往不应被优先推进,因为这类工作可能会耗费宝贵的人力、时间与技术资源,却无法带来与之匹配的商业价值。

在设定性能优化目标时,切忌给出过于理想化且不切实际的承诺。举例来说,声称能在两个月内将页面加载时间从 10 秒锐减至 1 秒,这样的目标不仅实现难度极大,而且从时间成本上看,老板通常难以接受。更为合理的做法是,提出明确且可实现的目标数值,比如将页面加载时间从 10 秒缩短至 5 秒,这意味着性能提升一倍。清晰明确的目标不仅有助于团队聚焦工作方向,还能为后续评估优化工作的收益提供直观对比依据。

此外,在设定目标值时,可采用一种策略:给出相对保守的预期目标。如此一来,当实际优化成果超出预期时,会给老板和团队带来额外惊喜。不过,务必注意的是,这并非鼓励夸大其词或盲目承诺。在设定目标前,团队必须对技术可行性、资源投入及潜在挑战进行充分评估,做到心中有数。毕竟,将性能耗时从 2 秒降低到 1 秒,其难度远非从 10 秒降低到 5 秒可比,其中涉及到更为复杂的技术瓶颈突破与系统架构调整。总之,性能优化工作需在合理规划、精准预期与切实可行的基础上稳步推进,以实现商业价值与用户体验的双赢。

参考文献与致谢

本文在写作针对性能检测工具查阅了相关资源和文章,感谢以下文章和作者: ① 胡哥有话说 【前端晋升答辩-性能优化篇范式】(juejin.cn/post/712698...)

本文主要结合过去6年从小程序开发、Web业务开发、互动开发经验中总结的一些实践经验,未必适合所有的前端业务,还请各位大佬因地制宜,接受任何合理的批评与建议,欢迎各位大佬多多留言。

相关推荐
蓝天白云下遛狗13 分钟前
goole chrome变更默认搜索引擎为百度
前端·chrome
come1123436 分钟前
Vue 响应式数据传递:ref、reactive 与 Provide/Inject 完全指南
前端·javascript·vue.js
前端风云志1 小时前
TypeScript结构化类型初探
javascript
musk12121 小时前
electron 打包太大 试试 tauri , tauri 安装打包demo
前端·electron·tauri
翻滚吧键盘2 小时前
js代码09
开发语言·javascript·ecmascript
万少2 小时前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
OpenGL2 小时前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl023 小时前
java web5(黑马)
java·开发语言·前端
Amy.Wang3 小时前
前端如何实现电子签名
前端·javascript·html5
海天胜景3 小时前
vue3 el-table 行筛选 设置为单选
javascript·vue.js·elementui