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

简介: 前端性能优化相关的“技能点”笔者之前也写过几篇,但是大多都是小打小闹。我重新整理了曾经使用过的性能优化手段...

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

页面资源预加载

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

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

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

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

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

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

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

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

    // 预加载 - 加载接下来所有的子应用,但是不显示
    prefetch()
}
AI 代码解读
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)))
}
AI 代码解读

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

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

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

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

注意:这个手段应该是没法用于普通页面开发行为的,而且像网上说的大多数通过 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>
AI 代码解读

然后你可以利用 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) });
},
AI 代码解读

这种方案的好处是减少首屏渲染的 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)
AI 代码解读

结合上面的图示,你应该可以“模拟”出为什么这么做能提升性能。
为了能更加“明示”,我们可以这么修改题目:如果一次性加载完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)
AI 代码解读

一张图片

相关文章
Resume Matcher:增加面试机会!开源AI简历优化工具,一键解析简历和职位描述并优化
Resume Matcher 是一款开源AI简历优化工具,通过解析简历和职位描述,提取关键词并计算文本相似性,帮助求职者优化简历内容,提升通过自动化筛选系统(ATS)的概率,增加面试机会。
155 18
Resume Matcher:增加面试机会!开源AI简历优化工具,一键解析简历和职位描述并优化
大厂面试官:聊下 MySQL 慢查询优化、索引优化?
MySQL慢查询优化、索引优化,是必知必备,大厂面试高频,本文深入详解,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验分享。
大厂面试官:聊下 MySQL 慢查询优化、索引优化?
详解队列在前端的应用,深剖JS中的事件循环Eventloop,再了解微任务和宏任务
该文章详细讲解了队列数据结构在前端开发中的应用,并深入探讨了JavaScript的事件循环机制,区分了宏任务和微任务的执行顺序及其对前端性能的影响。
面试必问的多线程优化技巧与实战
多线程编程是现代软件开发中不可或缺的一部分,特别是在处理高并发场景和优化程序性能时。作为Java开发者,掌握多线程优化技巧不仅能够提升程序的执行效率,还能在面试中脱颖而出。本文将从多线程基础、线程与进程的区别、多线程的优势出发,深入探讨如何避免死锁与竞态条件、线程间的通信机制、线程池的使用优势、线程优化算法与数据结构的选择,以及硬件加速技术。通过多个Java示例,我们将揭示这些技术的底层原理与实现方法。
205 3
Android经典面试题之图片Bitmap怎么做优化
本文介绍了图片相关的内存优化方法,包括分辨率适配、图片压缩与缓存。文中详细讲解了如何根据不同分辨率放置图片资源,避免图片拉伸变形;并通过示例代码展示了使用`BitmapFactory.Options`进行图片压缩的具体步骤。此外,还介绍了Glide等第三方库如何利用LRU算法实现高效图片缓存。
118 20
Android经典面试题之图片Bitmap怎么做优化
"面试通关秘籍:深度解析浏览器面试必考问题,从重绘回流到事件委托,让你一举拿下前端 Offer!"
【10月更文挑战第23天】在前端开发面试中,浏览器相关知识是必考内容。本文总结了四个常见问题:浏览器渲染机制、重绘与回流、性能优化及事件委托。通过具体示例和对比分析,帮助求职者更好地理解和准备面试。掌握这些知识点,有助于提升面试表现和实际工作能力。
127 1
「offer来了」浅谈前端面试中开发环境常考知识点
该文章归纳了前端开发环境中常见的面试知识点,特别是围绕Git的使用进行了详细介绍,包括Git的基本概念、常用命令以及在团队协作中的最佳实践,同时还涉及了Chrome调试工具和Linux命令行的基础操作。
「offer来了」浅谈前端面试中开发环境常考知识点
前端大模型应用笔记(一):两个指令反过来说大模型就理解不了啦?或许该让第三者插足啦 -通过引入中间LLM预处理用户输入以提高多任务处理能力
本文探讨了在多任务处理场景下,自然语言指令解析的困境及解决方案。通过增加一个LLM解析层,将复杂的指令拆解为多个明确的步骤,明确操作类型与对象识别,处理任务依赖关系,并将自然语言转化为具体的工具命令,从而提高指令解析的准确性和执行效率。
178 6
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试一线互联网企业时遇到了关于Redis分布式锁过期及自动续期的问题。尼恩对此进行了系统化的梳理,介绍了两种核心解决方案:一是通过增加版本号实现乐观锁,二是利用watch dog自动续期机制。后者通过后台线程定期检查锁的状态并在必要时延长锁的过期时间,确保锁不会因超时而意外释放。尼恩还分享了详细的代码实现和原理分析,帮助读者深入理解并掌握这些技术点,以便在面试中自信应对相关问题。更多技术细节和面试准备资料可在尼恩的技术文章和《尼恩Java面试宝典》中获取。
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等