30 前台页面的性能优化:如何实现前台页面的性能优化?
你好,我是杨文坚。
在运营搭建平台中,从页面功能维度,我们划分了五个功能模块,分别是页面搭建、页面编译和运行、页面发布流程、页面版本管理和页面渲染方式,在前五节课,我们实现了用户、物料和页面这三大功能维度,等于完成了搭建平台的核心功能。
接下来就要进入平台优化和扩展的设计开发阶段,会分成两个层次展开:实战类、增强类。
今天,我们先开始实战类的平台优化,要解决的问题是面向客户视角,如何进行前台页面的性能优化。
不过怎么思考和分析性能优化,很有讲究。因为我们前端程序员,提到页面性能优化一般有两种困惑,一种极端是,从来没遇到性能瓶颈的问题,觉得性能优化虚无缥缈,是很抽象的技术概念;另一种极端是,遇到了实际的性能问题,但是没有解决问题的思路。
如果你有类似的困惑,很正常。主要是单纯学习前端性能优化,都只是纸上谈兵,没应用到实际项目,或者没解决过实际问题,所以学性能优化有一个核心要点就是要“实用”。那我们接下来就从实际项目角度来讲解性能优化,以实用主义的思路来分析。
第一个要解决的问题是,什么时候需要做性能优化?
1. 什么时候需要做性能优化
在项目的开发周期中,前端开发部分在什么时候合适做性能优化呢?
其实这没有标准答案,但是有参考思路,我总结了两种思路。
- 前置优化思路
- 后置优化思路
前置优化思路
所谓的前置优化,就是在项目投入开发前,进行性能优化设计,也就是项目做技术方案设计的阶段,优先考虑优化设计。这时候,必须尽量考虑和预测可能会遇到哪些性能瓶颈点,针对这些高概率的问题,作为项目的实际需求,实行正式的技术方案设计和实际落地。
前置优化的思路,优点就是前期做了大量优化工作,避免后期出现高概率性能问题。但缺点就是加大了整体开发时间,不适合迭代频率高的业务,甚至容易陷入“过度优化”的困境。
过度优化,简单来讲就是“所做的优化工作,没用武之地”,可能没发挥优化作用,反而适得其反。
我举个例子,比如在搭建平台项目中,前端程序员评估预测一些极端边缘情况,可能会出现搭建页面依赖了几十个甚至上百个物料组件,导致编译Bundle文件过大,加载时间过长。
这时候,我们花时间做技术方案设计,最后实现了页面Bundle文件对物料的按需懒加载。但是,在实际使用过程中,基本没出现过这种极端情况,而且搭建平台完成历史使命,进行项目下线的时候,这个性能优化的措施一直没发挥作用。这种情况下,前置优化工作就是“过度设计”,浪费了前期的开发时间和资源。
后置优化思路
所谓“后置”优化,就是优先实现业务需求功能,后续看情况做性能优化。
后置优化的优点很明显,让实际开发时间可控,也不会出现“过度设计”的问题。当然缺点也显而易见,优先设计和开发业务需求功能,缺乏整体的性能优化设计,如果后续每次遇到性能问题,就只能“头疼医头,脚疼医脚”,容易引入大量冗余逻辑和代码。
比如,搭建平台已经完成功能开发正式投入使用,但是搭建的页面,图片太多,并发请求图片太慢,那么,我们需要根据图片并发请求问题做图片的懒加载;后续遇到另外的性能问题,比如物料组件太多,我们又需要对物料组件做按需懒加载。
这时候,存在两种资源懒加载的逻辑,造成了优化的功能逻辑和代码的重复开发,代码冗余。因为两种懒加载是能力,其实是能统一规划设计的。
两种思路分析
总的来说,这两种思路,你可以根据实际的项目情况选择。但是,不能墨守成规套用思路,应该尽量基于思路的优缺点,扬长避短,调整使用。
一般企业内的前端工作,大量是业务功能开发,这时候可以尽量选择“后置优化”的思路做功能开发,不过你要做相关的思路微调。例如在开发的时候,尽量设想一下可能出现的性能问题,然后做好预留优化操作的逻辑,不一定要做完整的优化设计,但是可以预留一些可操作的空间。
比如,你可以封装统一的Vue.js图片组件,在开发过程中,强制所有图片功能都要使用这个图片组件。这个图片组件,内部不一定要做性能优化措施,只是把所有图片逻辑以一个“空壳组件”统一管理。这样,你后续如果遇到图片并发加载过多的问题,只需要在这个统一的图片组件做一次优化处理就可以了,不需要逐一处理整个项目所有图片的显示逻辑。
好,我们现在知道在项目开发周期中,如何选择合适的时机,利用合适的思路做性能优化了。
不过无论选择前置优化,还是后置优化,我们都必须了解存在的性能问题,那么如何发现和分析性能问题呢?
2. 如何发现和分析性能问题
客户或者用户在使用前端页面的时候,碰到了性能问题,通常是不可能用技术层面的术语来反馈的,而是会直接描述遇到的现象,比如,“页面太卡了”“图片破了”等等。
这时候,我们开发者要排查技术问题,必须先还原问题场景,然后根据问题现象,发现和分析技术层面的问题点。
我总结了前端常见的性能问题场景,主要有四种。
- 打开页面卡顿
- 操作功能卡顿
- 图片等很久才显示
- 图片太多不显示
虽然有四种常见场景,但是分析套路都是一样的,我们就以“打开页面卡顿”这个问题现象为例,展开技术分析。
一般遇到性能问题,我们直接使用Chrome的DevTools,也就是开发工具控制台,来排查问题操作。
第一步,可以先从网络情况开始排查,切换到DevTools的网络面板“Network”,查看打开页面时候的网络请求情况。
从截图中可以看到,Chrome浏览器的开发者控制台中,红框展示的页面的网络请求情况。
如果请求数量很大或文件体积很大,同时,请求等待时间很长,“页面打开卡顿”问题在技术层面的原因,可以从“页面网络请求”耗时排查起来。
如果页面网络情况都在正常范围内,我们就需要进入第二步排查。
第二步,查看Chrome浏览器里控制台的性能情况。这时候你需要把DevTools切换到性能面板“Performance”,然后进行性能录制,重新加载页面,最后停止录制显示性能分析结果。
从截图中可以看出每一帧渲染的性能情况,同时,你可以看到每段JavaScript代码执行时的性能情况。这里的性能情况包括CPU、内存的占用,以及代码执行时间。
注意,这里的JavaScript代码的执行性能情况,可以分析到每一行代码的执行耗时。但是,这只是在Chrome浏览器的执行情况,不一定能代表所有浏览器。
你可以通过性能面板来分析,看看哪一个渲染阶段或者哪一段代码,影响了页面性能。如果性能面板的数据都不能排查出性能问题,我们就要进入第三步的排查。
第三步,如果觉得性能分析没问题,要看最后一个技术维度——帧率。
“帧率,Frame Rate,是用于测量显示帧数的度量。测量单位为“每秒显示帧数”,英文称为Frame Per Second,简称FPS,一般来说FPS用于描述影片、电子绘图或游戏每秒播放多少帧。”
浏览器显示的页面变化,是一帧帧的静态画面,按时间顺序组合而成的,如果每秒“闪过”画面的帧数接近60帧,属于很流畅的页面渲染,如果远远少于60帧,例如30FPS,就已经是卡顿的页面渲染了。
在控制台输入命令fps,你就可以调用帧率的显示工具。
打开了FPS工具,就可以在页面上看到实时的帧率情况。
这个帧率要结合性能面板里的信息内容综合判断,比如要结合每帧渲染情况、对应时间轴的JavaScript执行内容、内存情况和CPU情况等等。这样顺腾摸瓜,我们就能找到影响帧率的大致原因。
一般出现帧率的页面卡顿问题,很大可能是这四种情况。
- 页面里HTML渲染结构过于复杂。
- 页面运行的JavaScript可能有死循环代码。
- setInterval或requestAnimationFrame的不规范使用。
- 页面有用Canvas渲染2D或3D的内容,且内容过于复杂。
通过三步分析,基于Chrome的“网络面板”“性能面板”和“帧率情况”,我们就可以综合分析出大部分打开页面的卡顿现象。
总的来说,一旦出现性能相关的问题,比如页面卡顿、操作卡顿或者是图片加载不出来,我们要做的第一件事就是去做问题重现,根据发现者的操作,尽量还原出一模一样的问题场景;然后用Chrome的DevTools去发现技术层面的问题点,分析问题的技术原因。
到现在,我们发现和分析了性能问题的原因,无论是选择前置优化还是后置优化,都是要做优化操作。那么第三个问题就是如何对症下药做前端性能优化呢?
3. 如何对症下药做前端性能优化
分析了性能问题的技术原因后,你会发现一般与性能相关的技术原因,基本是围绕着网络、CPU、内存和帧率这四个技术角度展开的。
- 网络问题:请求量多,文件体积太大。
- CPU问题:执行复杂计算的JavaScript操作。
- 内存问题:代码逻辑原因,加载资源过大原因。
- 帧率问题:渲染内容复杂度原因,例如HTML结构、Canvas的2D和3D渲染的内容复杂度。
归纳一下可以分成三种技术问题。
- 第一类,网络并发或静态资源过大。
- 第二类,渲染内容过于复杂。
- 第三类,代码逻辑不规范。
那么对症下药。第三类问题,很简单,解决方案核心就是要处理代码不规范的问题,需要程序员在开发中严格规范代码写法。
这需要费些人力成本。例如要制定代码规范,采取对应的ESLint规则来限制,如果没有现成的ESLint规则,就要自己开发一个ESLint插件来规范。另外,还需要进行代码的Code Review检查,代码发布上线前,要进行代码人工审核检查。
第一、二类技术问题,我们可以统一处理,按需延迟加载或渲染内容,如果网络并发或静态资源过大,渲染内容过于复杂,可以把复杂大量的内容进行拆分,按需使用。这两类技术问题的解决方案,其实可以统称为“懒加载”,客户使用页面的时候,需要看到或用到什么内容,才进行加载渲染,没看到或者没使用到的,就进行按需懒加载。
这么讲有点抽象,我们用Vue.js演示一个“懒加载”的案例,页面有大量图片加载,导致请求过多的“卡顿问题”,要用懒加载的方式进行性能优化。
懒加载优化可以这么来设计实现。
基于懒加载技术方案设计图,我们可以用Vue.js的插件方式,实现统一管理图片懒加载。
// packages/portal-front/src/demos/lazyload-image/lazyload.ts
import { nextTick, toRaw } from 'vue';
import type { App, Plugin } from 'vue';
const imageQueueList: {
elem: HTMLImageElement;
src: string;
isLoaded: boolean;
}[] = [];
let container: HTMLElement | null = null;
/*
图片按需加载方法
- 计算图片是否在可视区域
- 如果在就进行按需加载
*/
function loadDisplayImage() {
if (!container) {
return;
}
const containerRect: DOMRect = container?.getBoundingClientRect() as DOMRect;
for (let i = 0; i < imageQueueList.length; i++) {
const item = imageQueueList[i];
if (item.isLoaded === true) {
continue;
}
const rect = item.elem.getBoundingClientRect();
const imageOffsetContainerTop = rect.y - containerRect.y;
if (
imageOffsetContainerTop > 0 &&
imageOffsetContainerTop < container?.clientHeight
) {
item.elem.setAttribute('src', item.src);
item.isLoaded = true;
}
}
}
// 容器滚动事件
function handleWheelEvent() {
loadDisplayImage();
}
// 监听可视容器的滚动事件
function listenScroll() {
container?.addEventListener('wheel', handleWheelEvent);
}
// 图片懒加载插件
const LazyloadPlugin: Plugin = {
install(app: App) {
// 注册自定义图片懒加载命令
app.directive('lazy-src', {
beforeMount(el, binding) {
// 收集所有需要懒加载的图片
imageQueueList.push({
elem: el as HTMLImageElement,
src: toRaw(binding?.value) as unknown as string,
isLoaded: false
});
}
});
// 注册懒加载可视容器命令
app.directive('lazy-container', {
beforeMount(el) {
// 注册懒加载可视区间容器DOM
container = el;
}
});
nextTick(() => {
if (container) {
listenScroll();
loadDisplayImage();
}
});
}
};
export default LazyloadPlugin;
基于图片懒加载插件,我们可以这么在组件中使用懒加载命令。
<!-- packages/portal-front/src/demos/lazyload-image/image-list.vue -->
<template>
<div class="image-list">
<div
class="image-item"
v-for="(item, index) in props.imageList"
v-bind:key="index"
>
<div class="image-preview">
<img class="image-image" v-lazy-src="item?.imageUrl" />
</div>
<div class="image-info">
<div class="image-title">{{ item.title }}</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
const props = defineProps<{
imageList: {
title: string;
imageUrl: string;
}[];
}>();
</script>
外部可视区间容器的使用代码。
<!-- packages/portal-front/src/demos/lazyload-image/app.vue -->
<template>
<div class="main" v-lazy-container>
<ImageList :imageList="imageList" />
</div>
</template>
<script lang="ts" setup>
import ImageList from './image-list.vue';
const imageList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13].map((i) => {
return {
imageUrl: `/public/cdn/svg/image-${i}.svg`,
title: `第${i + 1}张图片`
};
});
</script>
最后在渲染应用时候,使用我们刚刚开发的图片懒加载插件。
// packages/portal-front/src/demos/lazyload-image/index.ts
import { createApp } from 'vue';
import App from './app.vue';
import LazyloadPlugin from './lazyload';
const app = createApp(App, {});
app.use(LazyloadPlugin, { container: document.querySelector('body') });
app.mount('#app');
在代码中你可以看到,我们用了Vue.js的插件机制,写一个控制图片懒加载的插件。然后让图片使用了插件里定义的懒加载命令,当客户打开页面时,只渲染可视区间,可视区间第二屏以下的图片,不在可视区间内的就不加载渲染。
我们看最终效果图。首次打开页面的时候,判断和加载可视区间的图片。
滚动了可视区块后,让后续图片进入可视区间,按需加载需要显示的图片。
懒加载的实现方案,核心就是判断什么时候让需要使用的功能加载对应的内容,JavaScript、CSS和图片等等。
讲到这里,估计你也发现了,大部分前端性能优化工作,其实都是代码层面的优化工作,那么最后一个问题,Vue.js项目在代码开发时,如何优雅地进行性能优化呢?
4. Vue.js项目如何进行优雅的性能优化
从Vue.js图片懒加载的代码案例来看,我们的代码实现方式是基于一个Vue.js插件,再结合少量的图片自定义指令代码的修改。那么归纳一下,Vue.js项目性能优化的优雅实现思路,主要有两个要点。
- 要点一:优化逻辑尽量独立插件化。
- 要点二:尽可能少修改存量代码。
要点一,性能优化的逻辑代码,尽量实现独立插件化。在Vue.js项目中,我们要尽量借助Vue.js的框架机制,把性能优化逻辑插件化,形成“可插拔”的独立模块,这样就可以在开发中灵活使用和废弃。
要点二,如果插件逻辑不能覆盖全部优化操作,需要修改一些存量代码。例如案例图片的按需加载,要在图片资源定义的时候,使用插件里自定义的命令,收集需要懒加载的图片。这个时候,优化要尽量要控制这个代码修改量,因为存量代码修改量越少,引入新问题的概率就越小。
除了这两种优化方案,也还有让Vue.js项目优雅的优化方式,只不过有一定技术场景需要,比如能保证页面所有用到的Vue.js运行时来自全局变量,那么可以把Vue.js运行时变成独立的文件加载。
总结
我们从“前端性能优化”这个概念切入,先分析了性能优化在什么情况下使用,解决大部分前端开发对性能优化的常见困惑。在性能优化的具体实施中,有两种思路。
- 前置优化,项目投入开发前,预测可能出现的性能问题,然后做技术方案设计和实现。优点是能有规划提前做好问题规避,缺点是前期投入时间多。
- 后置优化,优先实现业务需求功能,后续看情况做性能优化。优点是项目时间不受影响,后续可以按需做性能优化,缺点是没有统一的技术规划和实现。当然这可以做前置准备,比如预留一些代码逻辑空间,方便之后做性能优化。
如何根据问题现象,分析背后的技术原因,我们主要借助Chrome浏览器的DevTools工具,从四个方面,分析当前页面的网络、CPU、内存和帧率的情况,定位具体的原因。
在Vue.js主题中,做前端性能优化有两个要点:优化逻辑能独立插件化、尽可能少修改存量代码。核心就是尽量让优化逻辑能独立化,减少对原有项目的干扰。
性能优化是无止境的,也没有一劳永逸的办法,所以我们选择了“实用主义”的思路,通过今天的学习,希望帮你解决了对前端性能优化的困惑,也希望你能掌握Vue.js性能优化的实现思路,以及前端性能优化的解决思路。
思考题
前端性能优化,很多都是关于优化静态资源请求的控制,我们是通过人工写代码懒加载的方式来处理,业界还有其他优化思路或方案吗?
期待在留言区看到你的思考,我们下节课见。
完整的代码在这里
- ifelse 👍(0) 💬(0)
学习打卡
2024-10-02