来玩,前端性能优化(+面试必问:宏任务和微任务)

news2024/9/23 21:30:43

前端性能优化相关的“技能点”笔者之前也写过几篇,但是大多都是小打小闹。我重新整理了曾经使用过的性能优化手段。本文介绍三种方案:页面资源预加载服务请求优化非首屏视图延迟加载

页面资源预加载

页面是不可能真正预加载的,但是有一个地方:入口代码中依赖的 js 模块。
一般来说,为了首屏的快速展示,我们并不会加载所有的代码/资源,而是当创建某个页面时再开始加载并执行页面相关的代码。

比如我老东家微店自研的脚手架就是这么做的,保证了 webview 页面的打开速度。还有的公司的 JSBundle 加载页面也是这么做的。

但是这个流程确实有可以优化的地方:让相关页面的 js 代码(下一个页面/所有子页面/最可能的页面)提前到前一个环节中,也就是在上一个页面展示的同时把下一个页面的 js 下载好,这样在进入下一个页面时页面创建到首屏渲染过程中就减少了 js 代码耗费时间。
预加载页面资源

如图就是笔者利用自己开发的微前端框架改造的一个老项目,它由两个子应用共同实现了5个页面 —— 我的意思是,这种优化手段是不可能用在普通的“页面开发级”实现中,必须是在框架或者更基础的底层实践中使用!

当一种手段没法支持我们的想法,那必然之路是:寻找更高层次/更底层的思路。比如我之前所在公司的脚手架是没法支持我“让页面间跳转和原生一样”的想法,但是如果能在页面之上还有一个东西去“控制”多个页面行为,就可以让“页面跳转”变成“单页应用路由跳转”。说实话这就是笔者写一个框架的原因。

笔者的做法是:
在“获取到当前页面路由”的时候,就去 异步 加载后面所有页面的 js 资源。

export const start = () => {
    // ...
    // 查找到符合当前url的子应用
    let app = currentApp()

    //...
    
    // 路由被触发了不止一次,我们可以加一个限制
    window.__CURRENT_SUB_APP__ = app.activeRule

    // 预加载 - 加载接下来所有的子应用,但是不显示
    prefetch()
}
import { parseHtml } from "./index"
import { getList } from "../const/subApps"

export const prefetch = async () => {
    // 获取到所有子应用的列表,但不包括当前正在显示的
    const list = getList().filter(item => !window.location.pathname.startsWith(item.activeRule))
    // 预加载剩下的所有子应用
    await Promise.all(list.map(async item => await parseHtml(item.entry, item.name)))
}

(这里是 async,但是调用方并没有 await)

与此同时,路由劫持会监听并找到当前的子应用,去执行它的生命周期、页面加载、事件监听等一系列操作。

注意:这个手段应该是没法用于普通页面开发行为的,而且像网上说的大多数通过 script 和 link 标签去预加载 js 资源的都是“单页”,不可能在多页面跳转中真正有效果。

预请求

预请求就是在不影响当页加载和交互的情况下,提前发出下一个页面的接口请求,并将结果缓存。以期望在下一个页面时消除网络请求时间对页面加载的影响,从而达到【直出/瞬开】的效果。

准确地说,预请求对于“触发请求时机”、“请求场景”、“数据有效性”有着严格的要求。比如笔者之前写过相关的文章,在文章中对于“列表页”到编辑页的数据进行了“预请求”操作。
老东家微店的某个项目截图
这当然不可能上线!说实话我的这个试验在数据量小的情况下是达到了效果的,但是数据量一大“命中率”就会大大降低,虽然我加了保险让它尽可能地不会影响到原先的性能,但是由此导致的开发投入远不能匹配收益。感兴趣的可以看看之前的这篇文章:用户体验新尝试&思考|让“跳转”加速。

我来总结几个点:

  1. 业务逻辑越复杂的页面,对预请求的要求和难度也就越大。如果我们对用户行为预判不够准确,会导致大量无效请求和没用的本地缓存,造成服务资源的浪费。慎重!(之前有考虑过利用其他手段比如 tensorflow 增加准确率,但是这完全是技术角度思考,从业务来说组里不可能会同意这种方法)
  2. 拿一个极端场景来说,商家在 pc 设置了某个商品,然后在 app 中又进行了编辑,那么在pc中的缓存时效怎么判断?假如说有一些对实时性要求高的场景比如秒杀信息,我们需要避免由于信息更新不及时导致的用户负反馈和不必要的损失。预请求缓存的数据需要设置合理的失效时间!
  3. 假如你使用了预请求,根据判断在用户进入列表页后正在缓存第一项的编辑态相关数据,用户也确实点击了第一项!但是,此时你的请求依然正在进行中。笔者在实践中使用了一种方案:构造一个通信模块,它能告诉我当前请求状态,如果请求依然在进行中,就等待,拿到数据后(会被缓存)通知我,我再从缓存中取数据。如果请求失败,同样会告诉我,我会按照超时重新进行“编辑页的正常请求”

当然,这种东西和有些优化手段一样,你不能只前端发力,可以拉着后端一起参谋,反正这个方案针对的就是“前后端交互时间”。

非首屏视图延迟加载

这个东西说的就多了,不过有一种方案笔者之前一直没写过:利用 textarea 标签让绝大部分非核心内容,比如非首屏展示区域“延后加载”。
没错就是先把 div 内容放在 textarea 标签中,然后用 js 慢慢取出来内容。

有一个需要注意的点就是,为了SEO考虑,你必须用多个 textarea 标签!

操作很简单:把 html 代码放入 textarea。
对于屏幕外我们首屏并不需要看到/非核心视图区域的 html 内容,存放到隐藏的 textarea 中,最好是 visibility: hidden;,让该 textarea 仍然占据本该渲染的位置(这一步是为了防止滚动条抖动)。

<textarea id='lazy-area' data-index='1'>
	<!-- 正常的html内容 -->
</textarea>

然后你可以利用 setTimeout 让 textarea 的 value 插入到文档中,或者监控视区变化 MutationObserver 当某个 textarea 进入可见区域再加载这部分的 html 节点。

observeListItem() {
  let observerVideo = new IntersectionObserver(
    (entries, observer) => {
        entries.forEach((entry, index) => {
            // 当移入指定区域内后....
            if(entry.intersectionRatio === 1) {
                let div = document.createElement('div');
				let area = document.querySelectorAll("#lazy-area")[index];
				div.innerHTML = area.value;
				area.parentNode.insertBefore(div,area);
				area.parentNode.removeChild(area);
				return;
            } else {
              if(cacheIndexs[entry.target.dataset.index].observe) {
                cacheIndexs[entry.target.dataset.index].observe = false;
              }
            }
            // observer.unobserve(entry.target);
          });
        }, 
        {
          // root: document.getElementById('scrollView'),
          rootMargin: '-16px -16px -16px -24px',
          threshold: 1
        }
    );
  document.querySelectorAll('#lazy-area').forEach(video => { observerVideo.observe(video) });
},

这种方案的好处是减少首屏渲染的 DOM 节点总数。

扩展:经典前端面试题

刚才提到利用 setTimeout 让 textarea 的 value 插入到文档中。这里突出一个点:首屏元素加载显示完成后再去加载后续元素。从而引出了“宏任务和微任务”的概念。

关于这个概念,笔者之前也写过相关文章:点此跳转。而且被很多人说过通俗易懂,但是笔者最近研究中发现那篇文章中说的还是“太绕了”。本文剩下的时间里给各位再梳理一遍:

进程?线程?

进程就是系统进行资源分配和调度的一个独立单位。一个进程内包含多个线程。

著名的【渲染进程】包含这些:

  1. GUI渲染线程(页面渲染)
  2. JS 引擎线程(执行 js 脚本)(和 GUI 线程互斥)
  3. 事件触发线程(eventloop 轮间处理线程)
  4. 事件(onclick)、定时器、ajax(独立线程)

这里有三个经典问题:

  • ajax?ajax是立即调用的,然后开一个线程去执行,成功后把回调放入宏任务队列。
  • “JS是单线程的”。应该是“js 的主线程是单线程的”,它会调用 API,这些 API 会再去开一个线程。
  • webworker?他是多线程,但并不是完全独立的,而是“主从线程”中的“从”。而且它并不能操作 DOM。

然后来一张图:
js线程&任务队列
有一个初级面试题是这么描述的:10w条数据怎么更高效的展示?答案当然是“切片加载”!

const total = 100000;
let oContainer = document.querySelector('#container');
const once = 2000;
const page = total/once;
const index = 0;

function insert(curTotal, curIndex) {
	if(curTotal < 0) return;
	// 在异步的基础上调用多次
	setTimeout(()=> {
		for(let i=0; i< once; i++) {
			let oLi = document.createElement('li');
			oLi.innerHTML = curIndex + i;
			oContainer.appendChild(oLi)
		}
		insert(curTotal - once, curIndex + once)
	}, 0)
}
insert(total, index)

结合上面的图示,你应该可以“模拟”出为什么这么做能提升性能。
为了能更加“明示”,我们可以这么修改题目:如果一次性加载完10w条数据,数据渲染完成的时间怎么获取?

let date = Date.now();
for(let i=0; i< 100000; i++) {
	let oLi = document.createElement('li');
	oLi.innerHTML = 1 + i;
	oContainer.appendChild(oLi)
}
console.log('时间', Date.now() - date)
setTimeout(()=> {
	console.log('渲染', Date.now() - date)
}, 0)

一张图片

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/132138.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux | PCIe Hotplug | 概念及工作原理的不完全总结

本文对 PCIe 热插拔的概念及工作原理进行不完全总结。 更新&#xff1a; 2022 / 12 / 31 Linux | PCIe Hotplug | 概念及工作原理的不完全总结热插拔组成部件实现代码通知式热插拔线程中断功耗管理意外移除错误处理整合移除BAR参考链接参考这里 1’ 2’ 3‘ 4’ 5 1992年初始…

postgresql数据库安装,备份还原

一.postgresql数据库安装 1.下载软件包 地址&#xff1a;PostgreSQL: File Browser 2.解压安装 [rootpostgresql u01]# tar -zxf postgresql-14.2.tar.gz安装环境 yum install -y perl-ExtUtils-Embed readline-devel zlib-devel pam-devel libxml2-devel libxslt-devel op…

(深度学习快速入门)第一章:深度学习概述、应用、学习路线和框架选择

文章目录一&#xff1a;基本概念&#xff08;1&#xff09;神经网络&#xff08;2&#xff09;感知器&#xff08;3&#xff09;深度学习&#xff08;4&#xff09;前向运算和反向传播二&#xff1a;DeepLearning学习路线三&#xff1a;深度学习应用&#xff08;1&#xff09;生…

基于Stream的Redis消息队列

目录一、消息队列二、基于List结构模拟消息队列基于List的消息队列的优点&#xff1a;基于List的消息队列的缺点&#xff1a;三、基于PubSub的消息队列基于PubSub的消息队列的优点&#xff1a;基于PubSub的消息队列的缺点&#xff1a;四、基于Stream的消息队列1、XADD语法2、XR…

C++ Primer笔记——allocator、unique_ptr和release、智能指针与动态数组、阻止拷贝

目录 一.P418 unique_ptr和release 二.P426 智能指针与动态数组 &#xff08;一&#xff09;.unique_ptr &#xff08;二&#xff09;.shared_ptr 三.P428 allocator &#xff08;一&#xff09;.申请空间 &#xff08;二&#xff09;.初始化构造 &#xff08;三&#…

活动星投票少儿模特大赛网络评选制作一次图文分组投票怎么制作

关于微信投票&#xff0c;我们现在用的最多的就是小程序投票&#xff0c;今天的网络投票&#xff0c;在这里会教大家如何用“活动星投票”小程序来进行投票。我们现在要以“国际车展少儿模特大赛”为主题进行一次投票活动&#xff0c;我们可以在在微信小程序搜索&#xff0c;“…

aws cloudformation 使用模板配置 ecs 蓝绿部署

参考资料 Perform ECS blue/green deployments through CodeDeploy using AWS CloudFormationAWS::CodeDeployBlueGreen 在之前的文章中&#xff0c;使用codepipeline中通过控制台的方式创建了ecs蓝绿部署的demo。实际上可以单独通过codedeploy完成ecs服务的蓝绿部署 参考官…

【ARMv8 SIMD和浮点指令编程】Libyuv I420 转 ARGB 流程分析

Libyuv 可以说是做图形图像相关从业者绕不开的一个常用库&#xff0c;它使用了单指令多数据流提升性能。以 ARM 处理为主线&#xff0c;通过 I420 转 ARGB 流程来分析它是如何流转的。 Libyuv 是一个开源项目&#xff0c;包括 YUV 的缩放和转换功能。 使用邻近、双线性或 box…

QThread、moveToThread用法详述

1.吐槽 QThread类提供了一种平台无关的方法对线程进行管理。但对于QThread类的熟练使用&#xff0c;即使是从事Qt开发多年的程序猿们&#xff0c;往往也会踩雷、入坑。总之&#xff1a;QThread类不好用、如果对该类理解不透&#xff0c;很容易导致程序崩溃。本人强烈建议&#…

(函数介绍)puts()函数

功能介绍 1. puts()函数用来向标准输出设备屏幕输出字符串并换行。 2. 函数的参数就是一个起始的地址&#xff0c;然后就从这个地址开始一直输出字符串&#xff0c;直到碰到\0就停止&#xff0c;然后这个\0是不进行输出的&#xff0c;是不能够算在里面的。与此同时&#xff…

十、字节缓冲流、字符流、转换流、对象操作流、对象序列化流

字节缓冲流 构造方法 字节缓冲流介绍 BufferedOutputStream&#xff1a;该类实现缓冲输出流.通过设置这样的输出流,应用程序可以向底层输出流写入字节,而不必为写入的每个字节导致底层系统的调用BufferedInputStream&#xff1a;创建BufferedInputStream将创建一个内部缓冲区数…

2022年为什么要学习C语言?

为什么学习c语言 为什么学C语言逻辑&#xff1f; 为什么要学习C语言&#xff1f; 学习C语言的主要理由有以下几点&#xff1a; C语言可以作为学习计算机程序设计语言的入门语言&#xff1b; C语言是编写操作系统的首选语言&#xff0c;与计算机硬件打交道时灵巧且高效&…

labelImag安装与使用及构造数据集

在做目标检测任务时&#xff0c;需要进行标注&#xff0c;选择了LabelImg作为标注工具&#xff0c;下面是安装及使用过程。 我们使用Anconda的虚拟环境进行安装&#xff0c;激活环境后&#xff0c;执行&#xff1a; pip install labelimg -i https://pypi.tuna.tsinghua.edu.c…

代码随想录算法训练营第四天 java : 24. 两两交换链表中的节点 ,19.删除链表的倒数第N个节点 ,面试题 02.07. 链表相交,142环形链表II

文章目录Leetcode 24. 两两交换链表中的节点题目链接本题思路需要注意的点AC 代码Leetcode 19.删除链表的倒数第N个节点题目链接需要注意的点AC代码Leetcode面试题 02.07. 链表相交题目链接这个略了Leetcode 142环形链表II题目链接难点:AC代码今日收获**一朵玫瑰正马不停蹄地成…

【Linux】Linux下基本指令(三)

作者&#xff1a;一个喜欢猫咪的的程序员 专栏&#xff1a;《Linux》 喜欢的话&#xff1a;世间因为少年的挺身而出&#xff0c;而更加瑰丽。 ——《人民日报》 目录 1. Linux基本指令&#xff1a;&#xff08;续&#xff09; 1.1zip指令和u…

极智编程 | C++模板函数

欢迎关注我的公众号 [极智视界]&#xff0c;获取我的更多笔记分享 大家好&#xff0c;我是极智视界&#xff0c;本文介绍一下 C模板函数。 模板函数是 C 中一种特殊的函数&#xff0c;它的类型参数列表用尖括号 <> 括起来&#xff0c;放在函数名的后面。使用模板函数&a…

Go 并发

来自 《Go 语言从入门到实战》 的并发章节学习笔记&#xff0c;欢迎阅读斧正&#xff0c;感觉该专栏整体来说对有些后端编程经验的来说比无后端编程经验的人更友好。。 Thread VS Groutine 创建时默认 Stack 大小&#xff1a;前者默认 1M&#xff0c;Groutint 的 Stack 初始化…

C语言可变参数与内存管理

有时&#xff0c;您可能会碰到这样的情况&#xff0c;您希望函数带有可变数量的参数&#xff0c;而不是预定义数量的参数。C 语言为这种情况提供了一个解决方案&#xff0c;它允许您定义一个函数&#xff0c;能根据具体的需求接受可变数量的参数。下面的实例演示了这种函数的定…

LeetCode题解 二叉树(八):404 左叶子之和;513 找树左下角的值;112 路径总和;113 路径总和II

二叉树 404 左叶子之和 easy 左叶子结点也好判断&#xff0c;若某结点属于左结点&#xff0c;且无子树&#xff0c;就是左叶子结点 即也如此&#xff0c;所以如果要判断&#xff0c;必然要从父结点下手&#xff0c;涉及到三层结点的处理 如果要使用递归法&#xff0c;要使用…

(二十三)大白话数据库服务器上的RAID存储架构的电池充放电原理

文章目录 1、RAID卡的缓存2、RAID卡的缓存里的数据会突然丢失怎么办?3、锂电池存在性能衰减问题1、RAID卡的缓存 服务器使用多块磁盘组成的RAID阵列的时候,一般会有一个RAID卡,这个RAID卡是带有一个缓存的,这个缓存不是直接用我们的服务器的主内存的那种模式,他是一种跟内…