首页 > 其他分享 >来玩,前端性能优化(+面试必问:宏任务和微任务)

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

时间:2023-09-02 15:37:41浏览次数:47  
标签:必问 const 请求 面试 任务 线程 let 页面 加载

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

页面资源预加载

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

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

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

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

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

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

笔者的做法是: 在“获取到当前页面路由”的时候,就去 异步 加载后面所有页面的 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)

export const parseHtml = async (entry, name) => {
	console.log(cache[name], 'cache')

    if(cache[name]) {
        return cache[name]
    }
    // 没有缓存的话去请求资源:资源加载其实是一个get请求,我们去模拟这个过程
    
    return [dom, allScripts]
}

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

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

预请求

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

准确地说,预请求对于“触发请求时机”、“请求场景”、“数据有效性”有着严格的要求。比如笔者之前写过相关的文章,在文章中对于“列表页”到编辑页的数据进行了“预请求”操作。

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

这当然不可能上线!说实话我的这个试验在数据量小的情况下是达到了效果的,但是数据量一大“命中率”就会大大降低,虽然我加了保险让它尽可能地不会影响到原先的性能,但是由此导致的开发投入远不能匹配收益。感兴趣的可以看看之前的这篇文章:用户体验新尝试&思考|让“跳转”加速

我来总结几个点:

  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。

然后来一张图:

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

有一个初级面试题是这么描述的: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)

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

标签:必问,const,请求,面试,任务,线程,let,页面,加载
From: https://blog.51cto.com/u_15296224/7332801

相关文章

  • 关于windows定时任务备份mysql
    windows 定时一、右击我的电脑->选择管理->任务计划程序,打开计划任务二、开始创建任务计划。1、常规设置?都懂不再多说。2、触发器:新建->设置一个时间3、操作:新建->选择一个可执行程序,参数如果执行PHP备份mysql。首先mysql加入环境变量,直到mysql在命令行能执行如在path中新......
  • 软件测试面试题
    软件测试的流程?考察目的:软件测试基础参考答案:需求评审(需求是否合理、是否可测)->测试计划(人、时间、业务点、资源)->测试设计(测试用例)->冒烟测试(准入测试,基本业务测试不通过直接打回)->测试执行(环境、工具搭建、用例执行)->bug提交->新版本发布(bug有没有验证、新功能......
  • Flink 1.17教程:任务槽Task Slots和并行度的关系
    任务槽TaskSlots在ApacheFlink中,任务槽(TaskSlots)是指可用于执行并行任务的资源单元。每个任务槽可以看作是一个可用的执行线程或处理单元,用于并行执行作业的不同部分。通俗来说,可以将任务槽想象成一个工作台,而每个工作台上都可以同时进行一项任务。任务槽的数量决定了同时可以......
  • Java集合面试之Queue篇
    Java集合面试之Queue篇(qq.com)1、队列是什么?队列是常用数据结构之一。是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,故为先进先出(FIFO,firstinfirstout)线性表。和栈一样,队列是一种操作受限制的线性表。2、队列的分类?Qu......
  • Java List常见面试题
    Java集合面试之List篇你好,面试官|我用JavaList狂怼面试官~(qq.com)本文涉及ArrayList与LinkedList区别、ArrayList扩容机制、CopyOnWriteArrayList特点、场景、思想ArrayList:基于数组实现的非线程安全的集合。实现RandomAccess接口,支持随机访问,查询元素快,插入,......
  • 20230829-面试题html+css5道题记录
    css预处理工具参考答案:CSS预处理器是一个能让你通过预处理器自己独有的语法来生成CSS的程序。css预处理器种类繁多,三种主流css预处理器是Less、Sass(Scss)及Stylus;它们各自的背景如下:Sass:2007年诞生,最早也是最成熟的CSS预处理器,拥有ruby社区的支持和compass这一最强大的css框......
  • 20230825-面试题html+css5篇简单记录
    html标签的类型(head,body,!Doctype)他们的作用是什么!DOCTYPE标签:它是指示web浏览器关于页面使用哪个HTML版本进行编写的指令.head:是所有头部元素的容器,绝大多数头部标签的内容不会显示给读者该标签下所包含的部分可加入的标签有base,link,meta,script,style和title......
  • android面试题:谈谈对Java中多态的理解
     Java中的多态是面向对象编程的一个重要特征,它允许同一个类型的对象在不同的情况下表现出不同的行为。多态是Java语言中实现代码复用、提高代码可维护性和可扩展性的重要手段。 多态的实现基于两个核心概念:继承和方法重写。在Java中,子类可以继承父类的方法,并且可以重写(覆......
  • 备战金九银十秋招面试,Android面试高频题合集,赶快收藏起来
    前言现在面试可以说是我们最常挂在嘴边的一个话题了,面试题、面试宝典、面试手册......各种Android面试题一搜一大把,根本看不完,而且现在的面试资料也都还可以,然后就放进了收藏夹吃灰,真到面试的时候,又被面试官问得脚趾扣地。人都是不满足于现状的,现在15K的薪资,下一份就想拿40K,甚至想......
  • 开发小技巧 - 合理使用Visual Studio 2022内置任务列表(TODO)
    前言在开发编码过程中经常会因为各种问题而打断自己的思绪和开发计划,可能会导致本来准备开发或者需要测试的功能到要上线的时候才想起来没有做完。这种情况相信很多同学都遇到过,咱们强大的VisualStudio内置了一个任务列表(TODO)能让我们当做待办清单功能使用,接下来我们快速了解一......