你应该如何引入JavaScript
vite build
当我们用vite build
时,默认会将依赖文件以modulepreload
的方式预加载。
<script type="module" crossorigin src="/assets/main-W15RtDcg.js"></script>
<link rel="preload" as="script" crossorigin href="/assets/modulepreload-polyfill-DaKOjhqt.js">
<link rel="preload" as="script" crossorigin href="/assets/vue-B55glXv2.js">
<link rel="preload" as="script" crossorigin href="/assets/pinia-Csf_hNVa.js">
<link rel="stylesheet" crossorigin href="/assets/main-DIVsyA-7.css">
其中甚至会增加一个modulepreload
的polyfill
。
(function polyfill() {// 1. 检测是否支持<link rel="modulepreload" >// 支持则return,即利用浏览器的默认行为,不再做额外处理const relList = document.createElement("link").relList;if (relList && relList.supports && relList.supports("modulepreload")) {return;}// 2. 不支持则利用js模拟,浓缩成processPreload函数// 2.1 处理已有的linksfor (const link of document.querySelectorAll('link[rel="modulepreload"]')) {processPreload(link);}// 2.2 监听可能新加的linksnew MutationObserver((mutations) => {for (const mutation of mutations) {if (mutation.type !== "childList") {continue;}for (const node of mutation.addedNodes) {if (node.tagName === "LINK" && node.rel === "modulepreload")processPreload(node);}}}).observe(document, { childList: true, subtree: true });function getFetchOpts(link) {const fetchOpts = {};if (link.integrity) fetchOpts.integrity = link.integrity;if (link.referrerPolicy) fetchOpts.referrerPolicy = link.referrerPolicy;if (link.crossOrigin === "use-credentials")fetchOpts.credentials = "include";else if (link.crossOrigin === "anonymous") fetchOpts.credentials = "omit";else fetchOpts.credentials = "same-origin";return fetchOpts;}function processPreload(link) {if (link.ep)return;link.ep = true;const fetchOpts = getFetchOpts(link);fetch(link.href, fetchOpts);}
})();
HTML中如何引入JavaScript
inline
内联,解析到即执行。
<script>const a = 1;
</script>
external
独立文件,解析到的时候,资源需要先下载再执行。阻塞HTML继续解析。
<script src="main.js"></script>
在下载JavaScript资源时没必要阻塞HTML解析。
<script async />
下载时继续HTML解析,下载完再立刻执行。
<script async src="main.js"></script>
适用于,JavaScript内容与DOM结构无关的资源。
<script defer />
下载时继续解析HTML,并等到document解析完之后再按书写顺序依次执行。
<script defer src="main.js"></script>
适用于,依赖了DOM结构的资源,一般如入口文件。
至此,解决了JavaScript的下载阻塞HTML解析的问题。
同时,JavaScript也发展到了模块时代。
JavaScript programs started off pretty small — most of its usage in the early days was to do isolated scripting tasks, providing a bit of interactivity to your web pages where needed, so large scripts were generally not needed. Fast forward a few years and we now have complete applications being run in browsers with a lot of JavaScript, as well as JavaScript being used in other contexts (Node.js, for example).
JavaScript modules - JavaScript | MDN
[图片来自: https://html.spec.whatwg.org/images/asyncdefer.svg]
<script type="module" />
默认是defer
的。
<script type="module" src="main.js"></script>
模块带来了静态依赖关系,在构建侧促进了tree-shaking
的应用发展。在浏览器侧,也被vite利用来做一个可以快速启动的dev-server
。
In addition to enabling the use of ES modules, Rollup also statically analyzes the code you are importing, and will exclude anything that isn't actually used. This allows you to build on top of existing tools and modules without adding extra dependencies or bloating the size of your project.
Since this approach can utilise explicit import
and export
statements, it is more effective than simply running an automated minifier to detect unused variables in the compiled output code.
Introduction | Rollup
This is essentially letting the browser take over part of the job of a bundler: Vite only needs to transform and serve source code on demand, as the browser requests it.
Why Vite | Vite
利用浏览器作依赖分析的弊端在于深层资源依赖的下载瀑布。
Even though native ESM is now widely supported, shipping unbundled ESM in production is still inefficient (even with HTTP/2) due to the additional network round trips caused by nested imports.
Why Vite | Vite
Even though the server has no problem handling them, the large amount of requests create a network congestion on the browser side, causing the page to load noticeably slower.
Dependency Pre-Bundling | Vite
nested_imports
这也是为什么还需要打包的理由,打包有一个重要的作用就是,把nested
抹平,使浏览器尽可能少地发起网络动作,减少RoundTrip
。
All modern browsers support module features natively without needing transpilation. It can only be a good thing — browsers can optimize loading of modules, making it more efficient than having to use a library and do all of that extra client-side processing and extra round trips. It does not obsolete bundlers like webpack, though — bundlers still do a good job at partitioning code into reasonably sized chunks, and are able to do other optimizations like minification, dead code elimination, and tree-shaking.
JavaScript modules - JavaScript | MDN
不用等HTML解析到<script>
标签才下载,而是更早地下载。
<link rel="preload" />
提前下载。
The preload
value of the <link> element's rel attribute lets you declare fetch requests in the HTML's <head>, specifying resources that your page will need very soon, which you want to start loading early in the page lifecycle, before browsers' main rendering machinery kicks in. This ensures they are available earlier and are less likely to block the page's render, improving performance. Even though the name contains the term load, it doesn't load and execute the script but only schedules it to be downloaded and cached with a higher priority.
rel=preload - HTML | MDN
<head><meta charset="utf-8" /><title>JS and CSS preload example</title><link rel="preload" href="style.css" as="style" /><link rel="preload" href="main.js" as="script" /><link rel="stylesheet" href="style.css" />
</head><body><h1>bouncing balls</h1><canvas></canvas><script src="main.js" defer></script>
</body>
去掉也毫无影响,只是作为性能优化手段指示性地告诉浏览器,我马上要用到这个资源,你去下载吧!这对于深层资源效果更加明显,原本需要下载A,解析A,然后发现A中依赖B,再去下载B,解析B,甚至CDEF,这将是巨大的嵌套地狱。通过preload指示性地声明所依赖的子资源,使浏览器提前感知并下载,在后续解析到时直接从cache中获取资源。
script
作为特殊的资源,代表普遍意义的脚本,但是
对于ESM
的script
,也即module
,更特殊地,我们可以提前解析优化。
preload
和prefetch
区别
- prefetch: followup/future/next navigation
- preload: current navigation
The prefetch
keyword for the rel attribute of the <link> element provides a hint to browsers that the user is likely to need the target resource for future navigations, and therefore the browser can likely improve the user experience by preemptively fetching and caching the resource. <link rel="prefetch">
is used for same-site navigation resources, or for subresources used by same-site pages.
rel=prefetch - HTML | MDN
脚本(classic script
)和模块(module
)的区别
- You need to pay attention to local testing — if you try to load the HTML file locally (i.e., with a
file://
URL), you'll run into CORS errors due to JavaScript module security requirements. You need to do your testing through a server. - Also, note that you might get different behavior from sections of script defined inside modules as opposed to in classic scripts. This is because modules use strict mode automatically.
- There is no need to use the
defer
attribute (see <script>attributes) when loading a module script; modules are deferred automatically. - Modules are only executed once, even if they have been referenced in multiple
<script>
tags. - Last but not least, let's make this clear — module features are imported into the scope of a single script — they aren't available in the global scope. Therefore, you will only be able to access imported features in the script they are imported into, and you won't be able to access them from the JavaScript console, for example. You'll still get syntax errors shown in the DevTools, but you'll not be able to use some of the debugging techniques you might have expected to use.
JavaScript modules - JavaScript | MDN
<link rel="modulepreload" />
The modulepreload
keyword, for the rel attribute of the <link> element, provides a declarative way to preemptively fetch a module script, parse and compile it, and store it in the document's module map for later execution.
Preloading allows modules and their dependencies to be downloaded early, and can also significantly reduce the overall download and processing time.
Links with rel="modulepreload"
are similar to those with rel="preload". The main difference is that preload
just downloads the file and stores it in the cache, while modulepreload
gets the module, parses and compiles it, and puts the results into the module map so that it is ready to execute.
use <link>
elements with rel="modulepreload"
for the main file and each of the dependency modules. This is much faster because the three modules all start downloading asynchronously and in parallel before they are needed. By the time main.js
has been parsed and its dependencies are known, they have already been fetched and downloaded.
rel="modulepreload" - HTML | MDN
index.html
main.js
modules/canvas.jssquare.js
<link rel="modulepreload" href="main.js" />
<link rel="modulepreload" href="modules/canvas.js" />
<link rel="modulepreload" href="modules/square.js" /><script type="module" src="main.js"></script>
modulepreload: parse and compile 都在线程池完成
preload: 主线程依然需要compile code
行为差异的关键就在于,preload
缺乏 module
信息,未提供给浏览器,所以不能做更极致的优化。
小结
总之,作为开发者,越精确地将我们知道的信息传递给浏览器,就能获得越多的优化。指示信息的缺失并不会影响程序的进行,但是提供指示信息却能将效率提高不少。
这种思想用在很多地方:
- 给
bundler
提供/*#__PURE__*/
纯函数标记,可以安全tree-shaking
- 给
v-for
提供key
,可以精确地做更新 - 给
addEventListener()
提供passive: true
,避免浏览器默认行为渲染阻塞。 - 给元素
CSS
提供will-change
属性,提示浏览器哪些属性即将发生变化,让浏览器提前优化渲染流程(如分配内存、创建复合层),从而减少样式变化时的卡顿,提升动画和交互性能。
If an event has a default action — for example, a wheel event that scrolls the container by default — the browser is in general unable to start the default action until the event listener has finished, because it doesn't know in advance whether the event listener might cancel the default action by calling Event.preventDefault(). If the event listener takes too long to execute, this can cause a noticeable delay, also known as jank, before the default action can be executed.
By setting the passive
option to true
, an event listener declares that it will not cancel the default action, so the browser can start the default action immediately, without waiting for the listener to finish. If the listener does then call Event.preventDefault(), this will have no effect.
EventTarget: addEventListener() method - Web APIs | MDN
More
当我们将眼光从浏览器再往前看,来到服务端将HTML
传给浏览器之前。
服务端了解HTML要比浏览器早得多。
那么作为网站的维护者,我们何必等到用户的浏览器拿到HTML,分析HTML,请求资源的时候再下发资源呢?
HTTP2 Push
在收到HTML的请求时,将所需要的资源主动推送给浏览器。但也有问题:
- 强制推送,也许浏览器端已有缓存,则不需要更新
HTTP3 103 Early Hints
做一次协商,服务端发送期望的资源链接,由浏览器决定具体是否请求。
The HTTP 103 Early Hints
informational response may be sent by a server while it is still preparing a response, with hints about the sites and resources that the server expects the final response will link to. This allows a browser to preconnect to sites or start preloading resources even before the server has prepared and sent a final response. Preloaded resources indicated by early hints are fetched by the client as soon as the hints are received.
103 Early Hints - HTTP | MDN
References
- https://developer.mozilla.org/
- https://html.spec.whatwg.org/
- https://vite.dev
- https://rollupjs.org