性能优化:webWorker做图片的预处理

1,138 阅读5分钟

前言

大家好,我是kk,一个热爱技术喜欢分享的“我不是程序猿kk”,今天来和大家聊一聊关于webWorker。

先来看一个例子: 我们都知道js是默认以单线程的方式运行的,那么当我们在一份html代码中,将script标签写到上边部分,并且里面存在着我们的事件循环机制,请问在宏任务执行完毕之前,页面会渲染吗?答:不会

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script>
        console.log(1)
        Promise.resolve(2).then(res => {
            console.log(2)
        })
        setTimeout(() => {
            console.log(3)
        })
        console.log(4)
    </script>
</head>
<body>
<div>
    <h2>
        hello
    </h2>
</div>
</body>
</html>

我们知道,在event loop中,首先执行同步代码,然后微任务,再是看页面是否需要渲染,最后开启下一次的事件循环也就是下一个宏任务。

那么1,4,2结束以后浏览器主线程就会从js转交给渲染线程,页面渲染完毕后,再打印3,这也就意味着,页面的渲染不会被宏任务影响,当我们的定时器设置5s以后,页面也不会在5s打印3完毕后才渲染。

那么至此,我们就知道了,浏览器的提供的主线程是可以被“各个部门所使用的”(css,js...)

加快页面渲染的小细节

加快页面渲染的方式有很多,做好各种各样的性能优化,但是在前言中我埋下了一个伏笔,就是将script标签写在了head中。

大家可以思考一下,这里存在什么问题,曾经我写过一篇关于回流重绘的文章,也提到了事实上回流重绘造成的开销并不大,最最需要注意的恰恰是我们的写代码行为习惯,写的代码是否规范。

最基本的,style写在上面,script写在下面,这是我们从学前端最开始就做的事情,现在回头想想你能明白了吗?js会阻塞页面的渲染,代码从上往下执行,js写下面,html先执行

结论 在正确的地方加载js脚本 = 不随意乱扔垃圾

问题

但是,有时我们不得不将js脚本放在上面,比如我们以CDN的方式引入一些源码,在页面加载之前需要做一些准备工作,请求一些资源数据。那么请求资源数据就会让页面加载变慢,因为执行js要时间,请求资源要时间,主线程被http占用,线程没有释放,渲染工作无法开始,这就导致了页面的留白(在DOM结构生成加载过多不合理的js脚本)

如何解决

async

async:让js的加载变成异步,浏览器加载js资源的同时会渲染html,但是当js资源加载完毕后,js代码就要执行,此时html的渲染会暂停等js执行完毕后再次渲染,js资源加载的过程是两个线程,浏览器开辟线程,渲染与加载并发进行。

小伙伴要问了,那资源加载完毕后为什么渲染线程就要让js先执行,而不像刚才一样并发执行呢?因为html线程和js线程天生就是互斥的,他俩会打架,毕竟js能修改dom结构,如果并发执行那就乱套了,谁先谁后都不清楚了,造成不安全渲染。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script async src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div>
    <h2>
        hello
    </h2>
</div>
</body>
</html>

defer

defer延迟执行,让js加载变成异步,当js加载完毕后,不会立即执行js,而是等到html渲染完毕后再执行

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script defer src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div>
    <h2>
        hello
    </h2>
</div>
</body>
</html>

到这里,小伙伴应该理解了这两个的区别,细心的小伙伴肯定发现了,引入vue的源码是不适合用defer的,不然就会导致你的html一团糟,因为他和html的关联非常强,但是如果我们引入的是一份axios就可以用defer。

webWorker

是什么

很好理解worker,工人,原本只招一个人,现在我们创建一个webWorker相当于多招了一个人。

Web Worker 为 Web 内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。此外,它们可以使用 XMLHttpRequest(尽管 responseXML 和 channel 属性总是为空)或 fetch(没有这些限制)执行 I/O。一旦创建,一个 worker 可以将消息发送到创建它的 JavaScript 代码,通过将消息发布到该代码指定的事件处理器(反之亦然)。

通俗来讲,就是你的代码从上往下执行,当他碰到了一个很复杂的事情要处理,我们可以创建一个worker,此时代码不停止,继续向下执行,这个复杂的问题交给worker同步执行,等执行完毕再合并到主分支。

应用场景

你的面试官是不是总问你如果页面有很多图片你怎么办?

  • 懒加载
  • 图片预加载,用webworker
  • ...

预加载

index.js

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="app">
    <h2>图片预加载</h2>
    <div id="pic">

    </div>
</div>

<script>
    let picBox = document.getElementById('pic')
    let arr = [
        "https://pic2.zhimg.com/v2-63dad01ff5c14e79d1622dc54865f5ed_r.jpg",
        "https://th.bing.com/th/id/R.3a3ebe796b0841704ffff16442613d8e?rik=XQq2A3xd2Nr%2frA&riu=http%3a%2f%2fpic.nipic.com%2f2007-09-20%2f200792010273943_2.jpg&ehk=XxEXRsFtDCEbNGL8FtH5LT1Sx8rET58iE0ClM0%2bkH1U%3d&risl=&pid=ImgRaw&r=0",
    ]
    // 为image.js脚本代码开辟一个新线程
    const worker = new Worker('./image.js')
    worker.postMessage(arr)


    // 监听来自子线程的消息
    worker.onmessage = function (e) {
        console.log('主线程',e)
        const img = new Image()
        img.src = window.URL.createObjectURL(e.data)
        picBox.appendChild(img)
    }
</script>
</body>
</html>

image.js

// 子线程的过程是异步的,不影响主线程的运行
self.onmessage = function (e){
    // 交给子线程工作
    for (let i = 0; i < e.data.length; i++){
        let xhr = new XMLHttpRequest();
        xhr.open('get', e.data[i], true);
        xhr.responseType = 'blob';
        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4 && xhr.status === 200){
                // 将图片地址发送给主线程
                self.postMessage(xhr.response);
            }
        }
        xhr.send();
    }
}

image.png

结语

如有疑问,欢迎私信讨论。