Javascript Workers
我们知道,JavaScript 是围绕单线程的概念设计的,缺乏实现像原生 App 那样的多线程模型所需的功能,比如共享内存。
浏览器使用单个线程(主线程)来运行网页中的所有 JavaScript、执行渲染页面和执行垃圾收集等任务。运行过多的 JavaScript 代码会阻塞主线程,延迟浏览器执行这些任务并导致糟糕的用户体验。
在 Web 中可以通过使用 Workers 在后台线程中运行脚本来实现类似多线程的模式,允许它们执行任务而不干扰主线程。Workers 是运行在单独线程上的整个 JavaScript 作用域,没有任何共享内存。
一般来说,Worker 可以让脚本在浏览器主线程之外的单独的线程上运行。如果你想要在 HTML 文档中引用一个<script>
标签的典型的 JavaScript 文件,它会运行在主线程上。如果主线程上有太多的计算,会拖慢网站的速度,造成交互卡顿和响应延迟。Web worker,Service worker 和 Worklet 都是让脚步运行在单独的线程上的。
Web worker
Web workers 是最常用的 worker 类型。它不像另外两种,它们除了运行在主线程外的特性外,没有一个特殊的应用场景。所以,Web worker 可以用于减少主线程上大量的线程活动。
一个很好的例子是前文提到的图像处理 Web 应用程序Squoosh[17],它使用 Web Worker 来进行图像处理任务,让主线程可供用户与应用程序进行不中断的交互。
PROXX[18]也使用了 Web worker 和 Service Worker 相关技术,具体可参考Proxx: Building Fast Web Applications[19]。
Web Worker 有以下几个使用注意点:
- 同源限制:分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
- DOM 限制:Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用 document、window、parent 这些对象。但是,Worker 线程可以使用 navigator 对象和 location 对象。
- 通信联系:Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息(postMessage)完成。
- 脚本限制:Worker 线程不能执行 alert()方法和 confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。
- 文件限制:Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。
Service worker
Service workers 主要是提供详细的浏览器和网络/缓存间的代理服务,如下图所以:
2014 年,W3C(万维网联盟)公布了 Service Worker 的相关草案,但真正在生产环境被 Chrome 支持是在 2015 年,比微信小程序要早两年。
下图展示了 Service workers 的生命周期:
而下面这张图则几乎涵盖了 Service workers 所有最重要的知识点:
HTTP 缓存与 Service Worker 缓存
可能你会好奇,用 Service Workers 来做缓存?HTTP 缓存它不香吗?我们可以简单看看这两者的区别:
- HTTP 缓存中,Web 服务器可以使用 Expires 首部来通知 Web 客户端,它可以使用资源的当前副本,直到指定的“过期时间”。反过来,浏览器可以缓存此资源,并且只有在有效期满后才会再次检查新版本。使用 HTTP 缓存意味着你要依赖服务器来告诉你何时缓存资源和何时过期(当然,HTTP 缓存控制还包括 cache-control,last-modified,etag 等字段)。
- Service Workers 的强大在于它们拦截 HTTP 请求的能力,接受任何传入的 HTTP 请求,并决定想要如何响应。在你的 Service Worker 中,可以编写逻辑来决定想要缓存的资源,以及需要满足什么条件和资源需要缓存多久。一切尽归你掌控!(所以,出于安全考虑,Service Workers 要求只能由 Https 承载)
注意事项
- Service worker 运行在 worker 上下文(self) --> 不能访问 DOM(这里其实和 Web Worker 是一样的);
- 它设计为完全异步,同步 API(如 XHR 和 localStorage)不能在 service worker 中使用;
- 出于安全考量,Service workers 只能由 HTTPS 承载;
- 某些浏览器的用户隐私模式,Service Worker 不可用;
- 其生命周期与页面无关(关联页面未关闭时,它也可以退出,没有关联页面时,它也可以启动)。
离线缓存
由于 Service Worker 的出现,我们不再尝试离线解决问题,而是让开发人员自己动手解决缓存问题。通过它可以控制缓存以及如何处理请求。这意味着您可以创建自己的模式。
缓存的模式有很多中,在离线指南[22]中,全面介绍了各种缓存的模式,在实践中,你可能需要根据 URL 和上下文同时使用多种模式。
当然,无论您做了多少缓存, Service Worker 进程都不会使用缓存,除非你告诉它何时以及如何使用。下图展示的是缓存优先的示意图:
其他一些缓存模式简单梳理如下:
调试
兼容性
- Service Worker[23]
- manifest[24]
可以看出,兼容性问题最大的其实还是在 manifest.json 的支持上。
Worklet
Worklet 是一个非常轻量级、高度具体的 worker。它。它们使我们作为开发人员能够连接到浏览器渲染过程的各个部分(钩子),让开发人员可以访问渲染管道的底层部分。
当一个 web 页面正在被渲染,浏览器经过很多步骤。在这里我们需要关注的有四步:Style,Layout,Paint 和 Composite。
在展示网页时,浏览器会执行多个步骤。在这里我们主要关注四个步骤:Style,Layout,Paint 和 Composite(合成)。
Paint 是浏览器将样式应用于每个元素的地方。与此渲染阶段挂钩的 Worklet 是 Paint Worklet。Paint Worklet 允许我们创建自定义图片,这个图片可以应用任何 CSS,比如 background-image 属性的值。
要创建一个 Worklet,就像所有 Worker 一样,我们在我们的主 javascript 文件中注册它,引用专用的 Worklet 文件。
/* main.js */ CSS.paintWorklet.addModule('myWorklet.js');
在我们的 Worklet 文件中,我们可以创建自定义图像。该 paint 方法的工作方式与 Canvas API 非常相似。这是一个简单的黑白渐变示例。
/* myWorklet.js */ registerPaint( 'myGradient', class { paint(ctx, size, properties) { var gradient = ctx.createLinearGradient(0, 0, 0, size.height / 3); gradient.addColorStop(0, 'black'); gradient.addColorStop(0.7, 'rgb(210, 210, 210)'); gradient.addColorStop(0.8, 'rgb(230, 230, 230)'); gradient.addColorStop(1, 'white'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, size.width, size.height / 3); } } );
最后,我们可以在 CSS 中使用这个新的 Worklet,我们创建的自定义图像将像任何其他背景图像一样应用。
div { background-image: paint(myGradient); }
除了 Paint Worklet,还有其他的 worklet 可以连接到渲染过程的其他阶段。Animation Worklet 连接到 Composite 阶段,而 Layout Worklet 连接到 Layout 阶段。
总结
Web worker,Service worker 和 worklet 都是将脚本运行在浏览器主线程之外单独的线程中,它们之间的区别是它们所应用的场景和他们的特性。
- Worklet 是浏览器渲染流中的钩子,可以让我们有浏览器渲染线程中底层的权限,比如样式和布局;
- Service worker 是浏览器和网络间的代理。通过拦截文档中发出的请求,service worker 可以直接请求缓存中的数据,达到离线运行的目的。
- Web worker 的通常目的是让我们减轻主线程中的密集处理工作。