Tìm hiểu cách bạn có thể sử dụng API Puppeteer để thêm các tính năng kết xuất phía máy chủ (SSR) vào máy chủ web Express. Điều tuyệt vời nhất là ứng dụng của bạn chỉ cần thay đổi rất nhỏ về mã. Chế độ không có giao diện người dùng sẽ thực hiện mọi thao tác nặng.
Trong một vài dòng mã, bạn có thể SSR bất kỳ trang nào và nhận được mã đánh dấu cuối cùng của trang đó.
import puppeteer from 'puppeteer';
async function ssr(url) {
const browser = await puppeteer.launch({headless: true});
const page = await browser.newPage();
await page.goto(url, {waitUntil: 'networkidle0'});
const html = await page.content(); // serialized HTML of page DOM.
await browser.close();
return html;
}
Tại sao nên sử dụng Headless Chrome?
Bạn có thể quan tâm đến Chrome không có giao diện người dùng nếu:
- Bạn đã tạo một ứng dụng web không được các công cụ tìm kiếm lập chỉ mục.
- Bạn muốn nhanh chóng đạt được thành công để tối ưu hoá hiệu suất JavaScript và cải thiện lần vẽ đầu tiên có ý nghĩa.
Một số khung như Preact cung cấp các công cụ giải quyết việc kết xuất phía máy chủ. Nếu khung của bạn có giải pháp kết xuất trước, hãy sử dụng giải pháp đó thay vì đưa Puppeteer và Chrome không có giao diện người dùng vào quy trình làm việc.
Thu thập dữ liệu trên web hiện đại
Các trình thu thập thông tin của công cụ tìm kiếm, nền tảng chia sẻ xã hội, thậm chí cả trình duyệt trước đây chỉ dựa vào mã đánh dấu HTML tĩnh để lập chỉ mục web và hiển thị nội dung. Web hiện đại đã phát triển thành một thứ hoàn toàn khác. Các ứng dụng dựa trên JavaScript sẽ vẫn tồn tại, điều này có nghĩa là trong nhiều trường hợp, công cụ thu thập thông tin có thể không thấy nội dung của chúng ta.
Googlebot, trình thu thập dữ liệu của Tìm kiếm, xử lý JavaScript trong khi vẫn đảm bảo không làm giảm trải nghiệm của người dùng khi truy cập vào trang web. Có một số điểm khác biệt và giới hạn mà bạn cần cân nhắc khi thiết kế trang và ứng dụng để phù hợp với cách các trình thu thập dữ liệu truy cập và hiển thị nội dung của bạn.
Tải trước trang
Tất cả trình thu thập thông tin đều hiểu HTML. Để đảm bảo trình thu thập thông tin có thể lập chỉ mục JavaScript, chúng ta cần một công cụ:
- Biết cách chạy tất cả loại JavaScript hiện đại và tạo HTML tĩnh.
- Luôn cập nhật khi web thêm tính năng.
- Chạy với ít hoặc không có nội dung cập nhật mã nào cho ứng dụng.
Nghe có vẻ ổn phải không? Công cụ đó chính là trình duyệt! Headless Chrome không quan tâm bạn sử dụng thư viện, khung hoặc chuỗi công cụ nào.
Ví dụ: nếu ứng dụng của bạn được tạo bằng Node.js, thì Puppeteer là một cách dễ dàng để làm việc với Chrome không có giao diện người dùng.
Bắt đầu với một trang động tạo HTML bằng JavaScript:
public/index.html
<html>
<body>
<div id="container">
<!-- Populated by the JS below. -->
</div>
</body>
<script>
function renderPosts(posts, container) {
const html = posts.reduce((html, post) => {
return `${html}
<li class="post">
<h2>${post.title}</h2>
<div class="summary">${post.summary}</div>
<p>${post.content}</p>
</li>`;
}, '');
// CAREFUL: this assumes HTML is sanitized.
container.innerHTML = `<ul id="posts">${html}</ul>`;
}
(async() => {
const container = document.querySelector('#container');
const posts = await fetch('/posts').then(resp => resp.json());
renderPosts(posts, container);
})();
</script>
</html>
Hàm SSR
Tiếp theo, hãy lấy hàm ssr()
từ trước đó và cải tiến một chút:
ssr.mjs
import puppeteer from 'puppeteer';
// In-memory cache of rendered pages. Note: this will be cleared whenever the
// server process stops. If you need true persistence, use something like
// Google Cloud Storage (https://firebase.google.com/docs/storage/web/start).
const RENDER_CACHE = new Map();
async function ssr(url) {
if (RENDER_CACHE.has(url)) {
return {html: RENDER_CACHE.get(url), ttRenderMs: 0};
}
const start = Date.now();
const browser = await puppeteer.launch();
const page = await browser.newPage();
try {
// networkidle0 waits for the network to be idle (no requests for 500ms).
// The page's JS has likely produced markup by this point, but wait longer
// if your site lazy loads, etc.
await page.goto(url, {waitUntil: 'networkidle0'});
await page.waitForSelector('#posts'); // ensure #posts exists in the DOM.
} catch (err) {
console.error(err);
throw new Error('page.goto/waitForSelector timed out.');
}
const html = await page.content(); // serialized HTML of page DOM.
await browser.close();
const ttRenderMs = Date.now() - start;
console.info(`Headless rendered page in: ${ttRenderMs}ms`);
RENDER_CACHE.set(url, html); // cache rendered page.
return {html, ttRenderMs};
}
export {ssr as default};
Các thay đổi chính:
- Thêm tính năng lưu vào bộ nhớ đệm. Lưu HTML đã kết xuất vào bộ nhớ đệm là chiến thắng lớn nhất để tăng tốc thời gian phản hồi. Khi trang được yêu cầu lại, bạn nên tránh chạy Chrome không có giao diện người dùng. Tôi sẽ thảo luận về các cách tối ưu hoá khác sau.
- Thêm tính năng xử lý lỗi cơ bản nếu thời gian tải trang hết hạn.
- Thêm lệnh gọi đến
page.waitForSelector('#posts')
. Điều này đảm bảo rằng các bài đăng tồn tại trong DOM trước khi chúng ta kết xuất trang đã chuyển đổi tuần tự. - Thêm khoa học. Ghi lại thời gian cần thiết để hiển thị trang không có giao diện người dùng và trả về thời gian hiển thị cùng với HTML.
- Dán mã vào một mô-đun có tên
ssr.mjs
.
Ví dụ về máy chủ web
Cuối cùng, đây là máy chủ express nhỏ kết hợp tất cả các thành phần. Trình xử lý chính hiển thị trước URL http://localhost/index.html
(trang chủ) và phân phát kết quả dưới dạng phản hồi. Người dùng sẽ thấy ngay các bài đăng khi truy cập vào trang vì mã đánh dấu tĩnh hiện là một phần của phản hồi.
server.mjs
import express from 'express';
import ssr from './ssr.mjs';
const app = express();
app.get('/', async (req, res, next) => {
const {html, ttRenderMs} = await ssr(`${req.protocol}://${req.get('host')}/index.html`);
// Add Server-Timing! See https://w3c.github.io/server-timing/.
res.set('Server-Timing', `Prerender;dur=${ttRenderMs};desc="Headless render time (ms)"`);
return res.status(200).send(html); // Serve prerendered page as response.
});
app.listen(8080, () => console.log('Server started. Press Ctrl+C to quit'));
Để chạy ví dụ này, hãy cài đặt các phần phụ thuộc (npm i --save puppeteer express
) và chạy máy chủ bằng Node 8.5.0 trở lên và cờ --experimental-modules
:
Dưới đây là ví dụ về phản hồi mà máy chủ này gửi lại:
<html>
<body>
<div id="container">
<ul id="posts">
<li class="post">
<h2>Title 1</h2>
<div class="summary">Summary 1</div>
<p>post content 1</p>
</li>
<li class="post">
<h2>Title 2</h2>
<div class="summary">Summary 2</div>
<p>post content 2</p>
</li>
...
</ul>
</div>
</body>
<script>
...
</script>
</html>
Một trường hợp sử dụng hoàn hảo cho API Thời gian của máy chủ mới
API Server-Timing (Thời gian máy chủ) sẽ thông báo các chỉ số hiệu suất của máy chủ (chẳng hạn như thời gian yêu cầu và phản hồi hoặc tra cứu cơ sở dữ liệu) trở lại trình duyệt. Mã ứng dụng khách có thể sử dụng thông tin này để theo dõi hiệu suất tổng thể của ứng dụng web.
Một trường hợp sử dụng hoàn hảo cho chỉ số Thời gian máy chủ là báo cáo thời gian Chrome không có giao diện người dùng cần để kết xuất trước một trang. Để làm việc đó, bạn chỉ cần thêm tiêu đề Server-Timing
vào phản hồi của máy chủ:
res.set('Server-Timing', `Prerender;dur=1000;desc="Headless render time (ms)"`);
Trên máy khách, bạn có thể sử dụng Performance API và PerformanceObserver để truy cập vào các chỉ số sau:
const entry = performance.getEntriesByType('navigation').find(
e => e.name === location.href);
console.log(entry.serverTiming[0].toJSON());
{
"name": "Prerender",
"duration": 3808,
"description": "Headless render time (ms)"
}
Kết quả về hiệu suất
Các kết quả sau đây kết hợp hầu hết các tối ưu hoá hiệu suất được thảo luận sau.
Trên một ứng dụng mẫu, Chrome không có giao diện người dùng mất khoảng một giây để hiển thị trang trên máy chủ. Sau khi trang được lưu vào bộ nhớ đệm, tính năng mô phỏng 3G chậm của DevTools sẽ đặt FCP ở mức nhanh hơn 8,37 giây so với phiên bản phía máy khách.
Hiển thị đầu tiên (FP) | First Contentful Paint (FCP) | |
---|---|---|
Ứng dụng phía máy khách | 4 giây | 11 giây |
Phiên bản SSR | 2,3 giây | ~2,3 giây |
Những kết quả này rất hứa hẹn. Người dùng sẽ thấy nội dung có ý nghĩa nhanh hơn nhiều vì trang được kết xuất phía máy chủ không còn dựa vào JavaScript để tải và hiển thị bài đăng.
Ngăn tình trạng mất nước trở lại
Bạn còn nhớ khi tôi nói rằng "chúng ta không thực hiện thay đổi nào về mã cho ứng dụng phía máy khách" không? Đó là lời nói dối.
Ứng dụng Express của chúng ta nhận một yêu cầu, sử dụng Puppeteer để tải trang vào chế độ không có giao diện người dùng và phân phát kết quả dưới dạng phản hồi. Tuy nhiên, cách thiết lập này có một vấn đề.
Chính JavaScript thực thi trong Chrome không có giao diện người dùng trên máy chủ sẽ chạy lại khi trình duyệt của người dùng tải trang trên giao diện người dùng. Chúng ta có hai vị trí tạo mã đánh dấu. #doublerender!
Để khắc phục vấn đề này, hãy cho trang biết HTML đã có sẵn.
Một giải pháp là yêu cầu JavaScript của trang kiểm tra xem <ul id="posts">
có trong DOM tại thời điểm tải hay không. Nếu có, bạn biết rằng trang đã được SSR và có thể tránh thêm lại bài đăng. 👍
public/index.html
<html>
<body>
<div id="container">
<!-- Populated by JS (below) or by prerendering (server). Either way,
#container gets populated with the posts markup:
<ul id="posts">...</ul>
-->
</div>
</body>
<script>
...
(async() => {
const container = document.querySelector('#container');
// Posts markup is already in DOM if we're seeing a SSR'd.
// Don't re-hydrate the posts here on the client.
const PRE_RENDERED = container.querySelector('#posts');
if (!PRE_RENDERED) {
const posts = await fetch('/posts').then(resp => resp.json());
renderPosts(posts, container);
}
})();
</script>
</html>
Tối ưu hoá
Ngoài việc lưu kết quả kết xuất vào bộ nhớ đệm, chúng ta có thể thực hiện nhiều hoạt động tối ưu hoá thú vị cho ssr()
. Một số chiến lược mang lại kết quả nhanh chóng, trong khi một số khác có thể mang tính đầu cơ hơn. Cuối cùng, lợi ích về hiệu suất mà bạn thấy có thể phụ thuộc vào loại trang mà bạn kết xuất trước và độ phức tạp của ứng dụng.
Huỷ các yêu cầu không cần thiết
Hiện tại, toàn bộ trang (và tất cả tài nguyên mà trang yêu cầu) được tải không điều kiện vào Chrome không có giao diện người dùng. Tuy nhiên, chúng ta chỉ quan tâm đến hai điều:
- Mã đánh dấu đã kết xuất.
- Các yêu cầu JS đã tạo ra mã đánh dấu đó.
Các yêu cầu mạng không tạo DOM là lãng phí. Các tài nguyên như hình ảnh, phông chữ, tệp định kiểu và nội dung đa phương tiện không tham gia vào việc tạo HTML của một trang. Các thành phần này tạo kiểu và bổ sung cấu trúc của trang nhưng không tạo cấu trúc đó một cách rõ ràng. Chúng ta nên yêu cầu trình duyệt bỏ qua các tài nguyên này. Điều này giúp giảm khối lượng công việc cho Chrome không có giao diện người dùng, tiết kiệm băng thông và có thể tăng tốc thời gian kết xuất trước cho các trang lớn hơn.
Giao thức DevTools hỗ trợ một tính năng mạnh mẽ có tên là Chặn mạng. Tính năng này có thể được dùng để sửa đổi các yêu cầu trước khi trình duyệt đưa ra yêu cầu.
Puppeteer hỗ trợ chặn mạng bằng cách bật page.setRequestInterception(true)
và nghe sự kiện request
của trang.
Điều đó cho phép chúng ta huỷ các yêu cầu đối với một số tài nguyên nhất định và cho phép các yêu cầu khác tiếp tục.
ssr.mjs
async function ssr(url) {
...
const page = await browser.newPage();
// 1. Intercept network requests.
await page.setRequestInterception(true);
page.on('request', req => {
// 2. Ignore requests for resources that don't produce DOM
// (images, stylesheets, media).
const allowlist = ['document', 'script', 'xhr', 'fetch'];
if (!allowlist.includes(req.resourceType())) {
return req.abort();
}
// 3. Pass through all other requests.
req.continue();
});
await page.goto(url, {waitUntil: 'networkidle0'});
const html = await page.content(); // serialized HTML of page DOM.
await browser.close();
return {html};
}
Tài nguyên quan trọng cùng dòng
Thông thường, bạn sẽ sử dụng các công cụ xây dựng riêng biệt (chẳng hạn như gulp
) để xử lý một ứng dụng và chèn CSS và JS quan trọng vào cùng dòng vào trang tại thời điểm tạo bản dựng. Điều này có thể tăng tốc độ hiển thị nội dung có ý nghĩa đầu tiên vì trình duyệt sẽ gửi ít yêu cầu hơn trong quá trình tải trang ban đầu.
Thay vì một công cụ xây dựng riêng biệt, hãy sử dụng trình duyệt làm công cụ xây dựng! Chúng ta có thể sử dụng Puppeteer để thao tác với DOM của trang, các kiểu nội tuyến, JavaScript hoặc bất kỳ nội dung nào khác mà bạn muốn đưa vào trang trước khi kết xuất trước trang đó.
Ví dụ này cho thấy cách chặn các phản hồi cho các tệp định kiểu cục bộ và nội tuyến các tài nguyên đó vào trang dưới dạng thẻ <style>
:
ssr.mjs
import urlModule from 'url';
const URL = urlModule.URL;
async function ssr(url) {
...
const stylesheetContents = {};
// 1. Stash the responses of local stylesheets.
page.on('response', async resp => {
const responseUrl = resp.url();
const sameOrigin = new URL(responseUrl).origin === new URL(url).origin;
const isStylesheet = resp.request().resourceType() === 'stylesheet';
if (sameOrigin && isStylesheet) {
stylesheetContents[responseUrl] = await resp.text();
}
});
// 2. Load page as normal, waiting for network requests to be idle.
await page.goto(url, {waitUntil: 'networkidle0'});
// 3. Inline the CSS.
// Replace stylesheets in the page with their equivalent <style>.
await page.$$eval('link[rel="stylesheet"]', (links, content) => {
links.forEach(link => {
const cssText = content[link.href];
if (cssText) {
const style = document.createElement('style');
style.textContent = cssText;
link.replaceWith(style);
}
});
}, stylesheetContents);
// 4. Get updated serialized HTML of page.
const html = await page.content();
await browser.close();
return {html};
}
This code:
- Use a
page.on('response')
handler to listen for network responses. - Stashes the responses of local stylesheets.
- Finds all
<link rel="stylesheet">
in the DOM and replaces them with an equivalent<style>
. Seepage.$$eval
API docs. Thestyle.textContent
is set to the stylesheet response.
Auto-minify resources
Another trick you can do with network interception is to modify the responses returned by a request.
As an example, say you want to minify the CSS in your app but also want to
keep the convenience having it unminified when developing. Assuming you've
setup another tool to pre-minify styles.css
, one can use Request.respond()
to rewrite the response of styles.css
to be the content of styles.min.css
.
ssr.mjs
import fs from 'fs';
async function ssr(url) {
...
// 1. Intercept network requests.
await page.setRequestInterception(true);
page.on('request', req => {
// 2. If request is for styles.css, respond with the minified version.
if (req.url().endsWith('styles.css')) {
return req.respond({
status: 200,
contentType: 'text/css',
body: fs.readFileSync('./public/styles.min.css', 'utf-8')
});
}
...
req.continue();
});
...
const html = await page.content();
await browser.close();
return {html};
}
Sử dụng lại một phiên bản Chrome trên nhiều lượt kết xuất
Việc khởi chạy một trình duyệt mới cho mỗi lần kết xuất trước sẽ tạo ra nhiều hao tổn. Thay vào đó, bạn nên chạy một thực thể và sử dụng lại thực thể đó để hiển thị nhiều trang.
Puppeteer có thể kết nối lại với một phiên bản Chrome hiện có bằng cách gọi puppeteer.connect()
và truyền URL gỡ lỗi từ xa của phiên bản đó. Để duy trì một thực thể trình duyệt chạy trong thời gian dài, chúng ta có thể di chuyển mã khởi chạy Chrome từ hàm ssr()
vào máy chủ Express:
server.mjs
import express from 'express';
import puppeteer from 'puppeteer';
import ssr from './ssr.mjs';
let browserWSEndpoint = null;
const app = express();
app.get('/', async (req, res, next) => {
if (!browserWSEndpoint) {
const browser = await puppeteer.launch();
browserWSEndpoint = await browser.wsEndpoint();
}
const url = `${req.protocol}://${req.get('host')}/index.html`;
const {html} = await ssr(url, browserWSEndpoint);
return res.status(200).send(html);
});
ssr.mjs
import puppeteer from 'puppeteer';
/**
* @param {string} url URL to prerender.
* @param {string} browserWSEndpoint Optional remote debugging URL. If
* provided, Puppeteer's reconnects to the browser instance. Otherwise,
* a new browser instance is launched.
*/
async function ssr(url, browserWSEndpoint) {
...
console.info('Connecting to existing Chrome instance.');
const browser = await puppeteer.connect({browserWSEndpoint});
const page = await browser.newPage();
...
await page.close(); // Close the page we opened here (not the browser).
return {html};
}
Ví dụ: công việc cron để kết xuất trước định kỳ
Để hiển thị một số trang cùng một lúc, bạn có thể sử dụng một thực thể trình duyệt dùng chung.
import puppeteer from 'puppeteer';
import * as prerender from './ssr.mjs';
import urlModule from 'url';
const URL = urlModule.URL;
app.get('/cron/update_cache', async (req, res) => {
if (!req.get('X-Appengine-Cron')) {
return res.status(403).send('Sorry, cron handler can only be run as admin.');
}
const browser = await puppeteer.launch();
const homepage = new URL(`${req.protocol}://${req.get('host')}`);
// Re-render main page and a few pages back.
prerender.clearCache();
await prerender.ssr(homepage.href, await browser.wsEndpoint());
await prerender.ssr(`${homepage}?year=2018`);
await prerender.ssr(`${homepage}?year=2017`);
await prerender.ssr(`${homepage}?year=2016`);
await browser.close();
res.status(200).send('Render cache updated!');
});
Ngoài ra, hãy thêm một lệnh xuất clearCache()
vào ssr.js:
...
function clearCache() {
RENDER_CACHE.clear();
}
export {ssr, clearCache};
Lưu ý khác
Tạo tín hiệu cho trang: "Bạn đang được hiển thị ở chế độ không có giao diện người dùng"
Khi trang của bạn đang được Chrome không có giao diện người dùng hiển thị trên máy chủ, logic phía máy khách của trang có thể sẽ cần biết điều đó. Trong ứng dụng của mình, tôi đã sử dụng lệnh gọi lại này để "tắt" các phần của trang không đóng vai trò trong việc hiển thị mã đánh dấu bài đăng. Ví dụ: tôi đã tắt mã tải lười firebase-auth.js. Không có người dùng nào để đăng nhập!
Thêm tham số ?headless
vào URL hiển thị là một cách đơn giản để cung cấp cho trang một móc:
ssr.mjs
import urlModule from 'url';
const URL = urlModule.URL;
async function ssr(url) {
...
// Add ?headless to the URL so the page has a signal
// it's being loaded by headless Chrome.
const renderUrl = new URL(url);
renderUrl.searchParams.set('headless', '');
await page.goto(renderUrl, {waitUntil: 'networkidle0'});
...
return {html};
}
Và trong trang, chúng ta có thể tìm tham số đó:
public/index.html
<html>
<body>
<div id="container">
<!-- Populated by the JS below. -->
</div>
</body>
<script>
...
(async() => {
const params = new URL(location.href).searchParams;
const RENDERING_IN_HEADLESS = params.has('headless');
if (RENDERING_IN_HEADLESS) {
// Being rendered by headless Chrome on the server.
// e.g. shut off features, don't lazy load non-essential resources, etc.
}
const container = document.querySelector('#container');
const posts = await fetch('/posts').then(resp => resp.json());
renderPosts(posts, container);
})();
</script>
</html>
Tránh làm tăng số lượt xem trang trong Analytics
Hãy cẩn thận nếu bạn đang sử dụng Analytics trên trang web của mình. Việc kết xuất trước trang có thể dẫn đến số lượt xem trang tăng cao. Cụ thể, bạn sẽ thấy số lượt truy cập gấp 2 lần, một lượt truy cập khi Chrome không có giao diện người dùng hiển thị trang và một lượt truy cập khác khi trình duyệt của người dùng hiển thị trang.
Vậy giải pháp là gì? Sử dụng tính năng chặn mạng để huỷ mọi yêu cầu cố gắng tải thư viện Analytics.
page.on('request', req => {
// Don't load Google Analytics lib requests so pageviews aren't 2x.
const blockist = ['www.google-analytics.com', '/gtag/js', 'ga.js', 'analytics.js'];
if (blocklist.find(regex => req.url().match(regex))) {
return req.abort();
}
...
req.continue();
});
Lượt truy cập vào trang sẽ không bao giờ được ghi lại nếu mã không bao giờ tải. Bùm 💥.
Ngoài ra, hãy tiếp tục tải các thư viện Analytics để biết thông tin chi tiết về số lượt kết xuất trước mà máy chủ của bạn đang thực hiện.
Kết luận
Puppeteer giúp bạn dễ dàng kết xuất trang phía máy chủ bằng cách chạy Chrome không có giao diện người dùng, đóng vai trò là trình bổ trợ, trên máy chủ web. "Tính năng" mà tôi yêu thích nhất của phương pháp này là bạn có thể cải thiện hiệu suất tải và khả năng lập chỉ mục của ứng dụng mà không cần thay đổi mã đáng kể!
Nếu bạn muốn xem một ứng dụng đang hoạt động sử dụng các kỹ thuật được mô tả ở đây, hãy xem ứng dụng devwebfeed.
Phụ lục
Thảo luận về thông tin trước đây
Rất khó để hiển thị phía máy chủ các ứng dụng phía máy khách. Khó đến mức nào? Bạn chỉ cần xem số lượng gói npm mà mọi người đã viết dành riêng cho chủ đề này. Có vô số mẫu, công cụ và dịch vụ có sẵn để giúp bạn triển khai SSR cho ứng dụng JS.
JavaScript đồng dạng / toàn cục
Ý tưởng của JavaScript toàn cục có nghĩa là: cùng một mã chạy trên máy chủ cũng chạy trên máy khách (trình duyệt). Bạn chia sẻ mã giữa máy chủ và ứng dụng và mọi người đều cảm thấy thư thái.
Chrome không có giao diện người dùng cho phép "JS đồng cấu trúc" giữa máy chủ và ứng dụng. Đây là một lựa chọn tuyệt vời nếu thư viện của bạn không hoạt động trên máy chủ (Node).
Công cụ kết xuất trước
Cộng đồng Node đã xây dựng rất nhiều công cụ để xử lý các ứng dụng SSR JS. Không có gì đáng ngạc nhiên! Cá nhân tôi nhận thấy rằng YMMV (tuỳ trường hợp) với một số công cụ này, vì vậy, hãy tìm hiểu kỹ trước khi quyết định sử dụng một công cụ. Ví dụ: một số công cụ SSR cũ hơn và không sử dụng Chrome không có giao diện người dùng (hoặc bất kỳ trình duyệt không có giao diện người dùng nào khác). Thay vào đó, các trình duyệt này sử dụng PhantomJS (còn gọi là Safari cũ). Điều này có nghĩa là các trang của bạn sẽ không hiển thị đúng cách nếu đang sử dụng các tính năng mới hơn.
Một trong những trường hợp ngoại lệ đáng chú ý là Tạo trước. Tính năng kết xuất trước rất thú vị vì sử dụng Chrome không có giao diện người dùng và đi kèm với lớp trung gian cho Express:
const prerender = require('prerender');
const server = prerender();
server.use(prerender.removeScriptTags());
server.use(prerender.blockResources());
server.start();
Xin lưu ý rằng tính năng Tải trước bỏ qua thông tin chi tiết về cách tải xuống và cài đặt Chrome trên các nền tảng khác nhau. Thông thường, việc này khá khó khăn. Đây là một trong những lý do khiến bạn nên sử dụng Puppeteer.