I’m currently building a browser-based pet-raising experience. It is not a conventional game; the goal is to give users something new to explore inside the app and encourage them to return. I have worked on similar projects before, and one recurring feature of this type of page is its abundance of elaborate animations. Those animations can also produce surprisingly large static files.
Large assets need a loading strategy. Ideally, an animation should appear immediately when the user reaches the part of the interface that needs it, rather than making the user wait for a download. The most practical approach is speculative loading: let the browser fetch resources ahead of time while there is still an opportunity to do so.
The browser’s speculative loading options
Browsers provide several related mechanisms, but they are intended for different situations:
- Prefetch downloads some or all of the resources needed by a document, or part of a document, before that content must be rendered. The resources can then be available sooner when rendering begins.
- Prerender goes further by actually rendering content that may be shown later. Depending on the implementation, navigation from the current page to the prepared page can appear almost instantaneous.
- Preconnect accelerates a future request to a particular origin by performing some or all of the connection setup in advance, including DNS resolution, TCP negotiation, and TLS negotiation.
Choosing between them matters. A resource needed for the current view should not be treated in the same way as a large animation that may only be needed several interactions later.
Six loading hints and what they are for
1. rel="preconnect": start the connection early
<link rel="preconnect"> tells the browser that a connection to a particular origin is likely to be needed soon. Before the actual HTTP request is sent, the browser may perform DNS resolution, the TCP handshake, and TLS negotiation. Removing those steps from the critical moment can reduce round trips and shorten the time before the response begins.
Preconnect is especially useful for the most important external origins. However, connecting to too many third-party domains can be counterproductive, because each speculative connection consumes network and system resources. It is better to reserve preconnect for critical connections. For less important origins, use <link rel="dns-prefetch"> instead and only perform the DNS portion in advance.
2. rel="dns-prefetch": resolve a domain ahead of time
When the browser requests a cross-origin resource, it first needs to turn the domain name into an IP address. This DNS lookup takes time before the actual request can begin.
<link rel="dns-prefetch"> is a hint that resources from a particular origin may be needed. The browser can resolve that origin’s DNS name ahead of time, saving the first part of the later request. Unlike preconnect, it does not establish the TCP or TLS connection.
3. rel="preload": fetch a resource required by the current page
Preload is designed for important resources that the current page is expected to need soon. The browser begins loading the resource as soon as it discovers the preload hint, often while it is still parsing the main HTML document. The preload request can run in parallel with the document download, allowing the browser to make better use of the available connection.
The as attribute identifies the resource type, such as as="style" or as="script". This gives the browser information it can use when assigning priorities and deciding how the resource fits into the rendering process.
Preload should be used carefully. If too many resources are marked as high priority, they can compete with the assets required for the first screen and make initial rendering slower. It is therefore more appropriate for critical resources needed by the current view than for optional, large files that may never be used.
The exact behavior can vary between browsers. Some implementations also take page loading state and user behavior into account when deciding when speculative work should happen. For example, a browser may start fetching a destination page when the user appears likely to click its link.
4. rel="modulepreload": prepare JavaScript modules
modulepreload is the module-specific counterpart to preload. It tells the browser which JavaScript modules used by the current page are important enough to download early.
Its general purpose resembles rel="preload", but the browser handles the module differently: in addition to fetching and caching the module, it can compile it and place it into the in-memory module map. This makes it useful when an application depends on JavaScript modules that should be ready as early as possible.
5. rel="prefetch": fetch resources for a possible future interaction
Prefetch is intended for resources that may be needed later, rather than resources required to render the current page immediately. The key distinction from preload is timing and priority. Preload begins when the browser encounters the hint, while prefetch is generally scheduled for periods when the browser considers itself idle or has spare capacity.
This makes prefetch a good fit for large animation files in an interactive application. The files can be downloaded in the background without being placed ahead of the assets needed for the first screen. Asynchronous components in Vue also commonly use this kind of future-oriented loading strategy.
6. rel="prerender": render a future page in advance
⚠️ This feature is deprecated and non-standard. It should not be used.
A historical example looked like this:
<link rel="prerender" href="/next-page">
The hint told the browser that the user might navigate to a particular page next. The browser could then download its resources and render the destination off-screen, making the eventual navigation appear immediate.
This older link-based prerendering mechanism has been deprecated. It has been replaced by NoState Prefetch, which retrieves resources needed by a future page without fully prerendering the page or executing its JavaScript. NoState Prefetch can still improve performance by moving resource loading earlier, but it does not provide the same instant transition as full prerendering.
Using prefetch for large animation assets
For a collection of large animation resources where the first-screen experience must remain unaffected, rel="prefetch" is usually the most suitable option. Once the application has obtained the complete resource list from an API, it can create and append link elements to the document’s <head>. The browser then decides when the downloads should actually take place.
const link = document.createElement('link')
link.rel = 'prefetch'
link.href = url
link.onload = () => {
/* ... */
}
link.onerror = () => {
/* ... */
}
document.head.appendChild(link)
The drawback is browser support. Although this feature is supported even by IE11, Safari does not support it in the same way. In Safari, adding this link does not cause the resource to be prefetched, so another loading path is required.
A fallback for Safari: fetch from a Worker
The simplest fallback is to load the files with requests. But there is another question: after onload, the rendering main thread may still be busy. Without browser support for prefetch, how can the application determine whether the rendering thread is idle?
requestIdleCallback provides a mechanism for running a callback when the main thread has idle time. It can be useful for scheduling background work without competing directly with rendering. Unfortunately, Safari has only supported this function in preview versions, not in its regular released browser versions.
If the application cannot reliably detect an idle main thread and also wants to avoid adding work to that thread, the remaining option is to move the loading operation into a Web Worker. The Worker fetches each URL, converts a successful response into a Blob, and creates an object URL that the main application can use.
worker.js
const loadStatic = async (urls) => {
const promiseList = []
for (const url of urls) {
const reqPromise = new Promise(async (resolve, reject) => {
try {
const response = await fetch(url)
if (response.ok) {
const blob = await response.blob()
const blobUrl = URL.createObjectURL(blob)
resolve(blobUrl)
} else {
console.error('[loadStatic] 请求失败,状态:', response.status)
reject({ url, error: response.statusText })
}
} catch (error) {
console.error('[loadStatic] 网络错误:', error)
reject({ url, error })
}
})
promiseList.push(reqPromise)
}
return Promise.allSettled(promiseList)
}
self.onmessage = (e) => {
const urls = e.data
loadStatic(urls)
.then((data) => {
postMessage({ status: 'completed', data })
})
.catch((error) => {
console.error(error)
postMessage({ status: 'error', data: error })
})
}
The main thread creates the Worker, sends it the resource list, and receives the result for each URL. Successful requests return a blob: URL; failed requests include the original URL and the error information.
main.js
const worker = new Worker('./worker.js');
worker.onmessage = function (event) {
console.log("收到 worker 返回的内容", event.data);
event.data.forEach(info => {
console.log(info);
// 加载成功: { status: 'fulfilled', value: 'blob:xxxxx' }
// 加载失败: { status: 'rejected', reason: { url: 'xxxxx', error: 'Not Found' } }
})
};
worker.onerror = function (e) {
worker.terminate.();
console.log("[usePrefetch] error: " + e.message);
};
const urls = [xxx, xxx, xxx];
worker.postMessage(urls);
Finally, feature detection can choose the native prefetch path where it is available and the Worker fallback elsewhere:
const supportsPrefetch = document.createElement('link').relList.supports('prefetch')
This gives Chrome and Safari different loading paths for the same set of large files. Combined with an appropriate CDN caching strategy, it allows optional animation assets to be prepared ahead of use without making them compete unnecessarily with the resources needed to render the first screen.