这是2024年的第74篇文章
( 本文阅读时间:15分钟 )
本文旨在探讨和分享多种预加载技术及其在提升网站性能、优化用户体验方面的应用。
01
前言
02
CDN动态加速
在开始介绍预加载之前,其实开发者可以通过 CDN 动态加速优化用户与服务器的建连、内容传输时间。CDN 通常被用来加速静态资源的传输,比如图像、JavaScript 和 CSS,这个大部分开发者非常熟悉,但现代的 CDN 技术已经不仅仅局限于静态内容的优化,大部分 CDN 厂商可以利用其全球广泛分布的边缘节点服务器为网站提供动态内容的访问加速。
03
dns-prefetch DNS 预解析
<link rel="dns-prefetch" href="//example.com">
04
preconenct 域名预建连
<link rel="preload" href="styles.css" as="style">
<link rel="preload" href="main.js" as="script">
<link rel="preload" href="image.jpg" as="image">
<link rel="prefetch" href="next-page-image.jpg">
<link rel="prefetch" href="next-page-script.js">
05
prerender 预渲染
<link rel="prerender" href="https://example.com/next-page">
06
根据用户行为 prefetch 下一跳页面
function App() {
return (
<div className="App">
<h1>Product List</h1>
<div className="product-list">
<Product id="1" name="Product 1" imageUrl="https://via.placeholder.com/150" prefetchUrl="/next-page-1" />
<Product id="2" name="Product 2" imageUrl="https://via.placeholder.com/150" prefetchUrl="/next-page-2" />
<Product id="3" name="Product 3" imageUrl="https://via.placeholder.com/150" prefetchUrl="/next-page-3" />
</div>
</div>
);
}
const Product = ({ id, name, imageUrl, prefetchUrl, delay=200 }) => {
const [prefetchTimeout, setPrefetchTimeout] = useState(null);
const handleMouseOver = () => {
const timeout = setTimeout(() => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = prefetchUrl;
link.credentials = 'include';
document.head.appendChild(link);
}, delay);
// 防止用户快速
setPrefetchTimeout(timeout);
};
const handleMouseOut = () => {
// 如果过度发 prefetch 请求
clearTimeout(prefetchTimeout);
};
return (
<div className="product"
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}>
<img src={imageUrl} alt={name} loading="lazy" />
<p>{name}</p>
</div>
);
}
6.1 添加 credentials 属性,携带 cookie
<link rel="prefetch" href="..." as="script" credentials="include">
6.2 服务器设置缓存
app.get('/next-page', (req, res) => {
const purposeHeader = req.headers['purpose'] || req.headers['sec-purpose'];
if (purposeHeader === 'prefetch') {
res.set('Cache-Control', 'max-age=10'); // 设置缓存策略
console.log('Prefetch request detected, setting cache.');
} else {
console.log('Regular request detected, no cache.');
}
res.send(`
<h1>Next Page Content</h1>
<p>This is the next page that was prefetched.</p>`
);
});
07
页面与首屏请求并行加载
08
Speculation Rules API
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Speculation Rules API</title>
<!-- Speculation rules -->
<script type="speculationrules">
{
"prefetch": []
}
</script>
</head>
<body>
<a href="/page1.html" data-prefetch-url="/page1.html">Go to Page 1</a>
<a href="/page2.html" data-prefetch-url="/page2.html">Go to Page 2</a>
<a href="/page3.html" data-prefetch-url="/page3.html">Go to Page 3</a>
<script>
function addPrefetchRule(url) {
const speculationRulesScript = document.querySelector('script[type="speculationrules"]');
const rules = JSON.parse(speculationRulesScript.textContent);
// 检查 rule 是不是已经设置
if (!rules.prefetch.some(rule => rule.urls.includes(url))) {
rules.prefetch.push({
"source": "list",
"urls": [url]
});
// 更新 speculation rules
speculationRulesScript.textContent = JSON.stringify(rules);
console.log(`Prefetch rule added for: ${url}`);
}
}
// 鼠标 hover 时候添加
document.querySelectorAll('a[data-prefetch-url]').forEach(link => {
link.addEventListener('mouseover', () => {
const url = link.getAttribute('data-prefetch-url');
addPrefetchRule(url);
});
});
</script>
</body>
</html>
09
页面部分内容提前返回
9.1 流式渲染简介
const http = require('http');
http
.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/html',
'Transfer-Encoding': 'chunked',
});
function renderChunk(chunk) {
res.write(`<div>${chunk}</div>`);
}
renderChunk('Loading...');
setTimeout(() => {
renderChunk('Chunk 1');
}, 1000);
setTimeout(() => {
renderChunk('Chunk 2');
}, 2000);
setTimeout(() => {
renderChunk('Chunk 3');
}, 3000);
setTimeout(() => {
renderChunk('done!');
res.write('</body></html>');
res.end();
}, 4000);
})
.listen(3000, () => {
console.log('Server listening on port 3000');
});
9.2 开启流式渲染后的新思路
9.3 提前返回 preconnenct、preload 标签
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Optimized Page</title>
<!-- Preconnect to an external domain -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Preload a critical CSS file -->
<link rel="preload" href="/styles/main.css" as="style">
<!-- Preload an important image -->
<link rel="preload" href="/images/hero-banner.jpg" as="image">
</head>
<body>
const http = require('http');
const path = require('path');
const fs = require('fs');
const filePath = path.join(__dirname, 'public', 'static.html');
http
.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/html',
'Transfer-Encoding': 'chunked',
});
function renderChunk(chunk) {
res.write(`<div>${chunk}</div>`);
}
fs.readFile(filePath, 'utf8', (err, firstFragment) => {
// 返回静态部分,浏览器提前建连、加载
renderChunk(firstFragment);
});
renderChunk('Loading...');
setTimeout(() => {
// 复杂的服务端计算
renderChunk('done!');
res.write('</body></html>');
res.end();
}, 4000);
})
.listen(3000, () => {
console.log('Server listening on port 3000');
});
9.4 根据数据生成 preload html 片段
const http = require('http');
http
.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/html',
'Transfer-Encoding': 'chunked',
});
function renderChunk(chunk) {
res.write(`<div>${chunk}</div>`);
}
setTimeout(async () => {
const firstData = await getFirstScreenData();
renderChunk(`
<link rel="preload" href="${firstData.imgSrc}" as="image">
`);
}, 1000);
setTimeout(() => {
// 复杂的服务端计算
renderChunk('done!');
res.write('</body></html>');
res.end();
}, 4000);
})
.listen(3000, () => {
console.log('Server listening on port 3000');
});
9.5 http header 返回后续域名 preconenct
HTTP/2 200 OK
Content-Type: text/html
Link: <https://example.com>; rel=preconnect, <https://fonts.googleapis.com>; rel=preconnect
app.get('/', (req, res) => {
// Set Link headers for preconnect
res.set('Link', [
'<https://example.com>; rel=preconnect',
'<https://fonts.googleapis.com>; rel=preconnect'
].join(', '));
// Flush headers to send them immediately
res.flushHeaders();
// Stream the HTML file
const readStream = fs.createReadStream('content');
readStream.pipe(res);
});
10
HTTP/2 push
server {
listen 443 ssl http2;
server_name localhost;
ssl_certificate /path/to/your/certificate.crt;
ssl_certificate_key /path/to/your/private.key;
location / {
root /path/to/your/web/content;
index index.html;
# 启用 HTTP/2 推送
http2_push_preload on;
http2_push banner.jpg;
}
}
server {
listen 443 ssl http2;
server_name localhost;
ssl_certificate /path/to/your/certificate.crt;
ssl_certificate_key /path/to/your/private.key;
location / {
root /path/to/your/web/content;
# 启用 HTTP/2 推送
http2_push_preload on;
# 发送 HTML 文档的同时,告诉客户端推送资源
add_header Link "<banner.jpg.css>; as=image; rel=preload";
}
}
11
Early Hints
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
HTTP/1.1 103 Early Hints
Link: </style.css>; rel=preload; as=style
app.get('/', (req, res) => {
// Send 103 Early Hints response to client
res.status(103).set({
Link: [
'</styles/main.css>; rel=preload; as=style',
'</images/example.jpg>; rel=preload; as=image',
].join(', ')
}).end();
// Set up final response
const readStream = fs.createReadStream('content');
res.set('Content-Type', 'text/html');
// Stream the final response content
readStream.pipe(res);
});