Express वेब सर्वर में, सर्वर साइड रेंडरिंग (एसएसआर) की सुविधाएं जोड़ने के लिए, Puppeteer एपीआई का इस्तेमाल करने का तरीका जानें. सबसे अच्छी बात यह है कि आपके ऐप्लिकेशन के कोड में बहुत कम बदलाव करने पड़ते हैं. हेडलेस वर्शन, सभी ज़रूरी काम करता है.
कुछ लाइनों के कोड की मदद से, किसी भी पेज को एसएसआर किया जा सकता है और उसका फ़ाइनल मार्कअप पाया जा सकता है.
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;
}
बिना ग्राफ़िक यूज़र इंटरफ़ेस वाले Chrome का इस्तेमाल क्यों करें?
Headless Chrome में आपकी दिलचस्पी तब हो सकती है, जब:
- आपने ऐसा वेब ऐप्लिकेशन बनाया है जिसे सर्च इंजन इंडेक्स नहीं कर रहे हैं.
- आपको JavaScript की परफ़ॉर्मेंस को ऑप्टिमाइज़ करने और पहले काम के पेंट को बेहतर बनाने के लिए, तेज़ी से नतीजे चाहिए.
Preact जैसे कुछ फ़्रेमवर्क, ऐसे टूल के साथ शिप होते हैं जो सर्वर-साइड रेंडरिंग की सुविधा देते हैं. अगर आपके फ़्रेमवर्क में पेज को पहले से रेंडर करने का कोई समाधान है, तो अपने वर्कफ़्लो में Puppeteer और Headless Chrome को शामिल करने के बजाय, उस समाधान का इस्तेमाल करें.
आधुनिक वेब को क्रॉल करना
सर्च इंजन क्रॉलर, सोशल शेयरिंग प्लैटफ़ॉर्म, और ब्राउज़र, वेब को इंडेक्स करने और कॉन्टेंट दिखाने के लिए, स्टैटिक एचटीएमएल मार्कअप पर पूरी तरह से निर्भर रहे हैं. आधुनिक वेब, अब काफ़ी अलग हो गया है. JavaScript पर आधारित ऐप्लिकेशन का इस्तेमाल जारी रहेगा. इसका मतलब है कि कई मामलों में, क्रॉल करने वाले टूल हमारे कॉन्टेंट को नहीं देख पाएंगे.
Googlebot, Search का क्रॉलर है. यह JavaScript को प्रोसेस करता है. साथ ही, यह पक्का करता है कि इससे साइट पर आने वाले उपयोगकर्ताओं के अनुभव पर कोई असर न पड़े. अपने पेज और ऐप्लिकेशन डिज़ाइन करते समय, आपको अलग-अलग तरह के क्रॉलर के बीच के अंतर और सीमाओं का ध्यान रखना होगा. ऐसा इसलिए, ताकि आप कॉन्टेंट को ऐक्सेस और रेंडर करने के उनके तरीके के हिसाब से अपने पेज या ऐप्लिकेशन तैयार कर सकें.
पेजों को पहले से रेंडर करना
सभी क्रॉलर, एचटीएमएल को समझते हैं. यह पक्का करने के लिए कि क्रॉलर, JavaScript को इंडेक्स कर सकते हैं, हमें एक ऐसे टूल की ज़रूरत है जो:
- इसमें सभी तरह के नए JavaScript को चलाने और स्टैटिक एचटीएमएल जनरेट करने का तरीका पता होता है.
- वेब पर नई सुविधाएं जोड़ने पर, यह ऐप्लिकेशन अप-टू-डेट रहता है.
- आपके ऐप्लिकेशन में कोड के बहुत कम या बिना किसी अपडेट के काम करता है.
क्या आपको यह ठीक लगता है? वह टूल ब्राउज़र है! Headless Chrome को इससे कोई फ़र्क़ नहीं पड़ता कि आपने कौनसी लाइब्रेरी, फ़्रेमवर्क या टूल चेन का इस्तेमाल किया है.
उदाहरण के लिए, अगर आपका ऐप्लिकेशन Node.js के साथ बनाया गया है, तो Puppeteer का इस्तेमाल करके, बिना ग्राफ़िक यूज़र इंटरफ़ेस वाले Chrome के साथ आसानी से काम किया जा सकता है.
किसी ऐसे डाइनैमिक पेज से शुरू करें जो 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>
SSR फ़ंक्शन
इसके बाद, पहले दिए गए ssr()
फ़ंक्शन को थोड़ा बेहतर बनाएं:
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};
मुख्य बदलाव:
- कैश मेमोरी जोड़ी गई. रेंडर किए गए एचटीएमएल को कैश मेमोरी में सेव करने से, रिस्पॉन्स में लगने वाले समय को कम करने में मदद मिलती है. जब पेज का फिर से अनुरोध किया जाता है, तो बिना ग्राफ़िक यूज़र इंटरफ़ेस वाले Chrome को फिर से चलाने से बचा जा सकता है. हम बाद में अन्य ऑप्टिमाइज़ेशन के बारे में बात करेंगे.
- अगर पेज लोड होने में ज़्यादा समय लगता है, तो गड़बड़ी को मैनेज करने का बुनियादी तरीका जोड़ें.
page.waitForSelector('#posts')
में कॉल जोड़ें. इससे यह पक्का होता है कि सीरियलाइज़ किए गए पेज को डंप करने से पहले, पोस्ट, DOM में मौजूद हों.- विज्ञान जोड़ें. हेडलेस ब्राउज़र को पेज को रेंडर करने में लगने वाला समय लॉग करें. साथ ही, एचटीएमएल के साथ रेंडरिंग का समय दिखाएं.
- कोड को
ssr.mjs
नाम के मॉड्यूल में चिपकाएं.
वेब सर्वर का उदाहरण
आखिर में, यहां एक छोटा एक्सप्रेस सर्वर दिया गया है, जो इन सभी को एक साथ लाता है. मुख्य हैंडलर, यूआरएल http://localhost/index.html
(होम पेज) को पहले से रेंडर करता है और नतीजे को अपने रिस्पॉन्स के तौर पर दिखाता है. उपयोगकर्ताओं को पेज पर जाने के तुरंत बाद पोस्ट दिखती हैं, क्योंकि स्टैटिक मार्कअप अब रिस्पॉन्स का हिस्सा है.
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'));
इस उदाहरण को चलाने के लिए, डिपेंडेंसी (npm i --save puppeteer express
) इंस्टॉल करें और Node 8.5.0 और --experimental-modules
फ़्लैग का इस्तेमाल करके सर्वर चलाएं:
इस सर्वर से मिले रिस्पॉन्स का उदाहरण यहां दिया गया है:
<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>
नए Server-Timing API के इस्तेमाल का सबसे सही उदाहरण
Server-Timing API, ब्राउज़र को सर्वर की परफ़ॉर्मेंस मेट्रिक (जैसे, अनुरोध और रिस्पॉन्स के समय या डेटाबेस के लुकअप) की जानकारी देता है. क्लाइंट कोड, इस जानकारी का इस्तेमाल करके किसी वेब ऐप्लिकेशन की परफ़ॉर्मेंस को ट्रैक कर सकता है.
सर्वर-टाइमिंग का इस्तेमाल करने का सबसे सही उदाहरण यह है कि बिना ग्राफ़िक यूज़र इंटरफ़ेस वाले Chrome को किसी पेज को प्री-रेंडर करने में कितना समय लगता है. ऐसा करने के लिए, सर्वर रिस्पॉन्स में Server-Timing
हेडर जोड़ें:
res.set('Server-Timing', `Prerender;dur=1000;desc="Headless render time (ms)"`);
क्लाइंट पर, इन मेट्रिक को ऐक्सेस करने के लिए परफ़ॉर्मेंस एपीआई और PerformanceObserver का इस्तेमाल किया जा सकता है:
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)"
}
परफ़ॉर्मेंस के नतीजे
यहां दिए गए नतीजों में, परफ़ॉर्मेंस के ज़्यादातर ऑप्टिमाइज़ेशन शामिल हैं. इनके बारे में बाद में बताया गया है.
किसी ऐप्लिकेशन के उदाहरण पर, बिना ग्राफ़िक यूज़र इंटरफ़ेस वाले Chromium ब्राउज़र को सर्वर पर पेज को रेंडर करने में करीब एक सेकंड लगता है. पेज के कैश मेमोरी में सेव होने के बाद, DevTools 3G स्लो इम्यूलेशन की मदद से, क्लाइंट-साइड वर्शन की तुलना में FCP को 8.37 सेकंड तेज़ किया जाता है.
फ़र्स्ट पेंट (एफ़पी) | First Contentful Paint (FCP) | |
---|---|---|
क्लाइंट-साइड ऐप्लिकेशन | 4 सेकंड | 11 सेकंड |
SSR वर्शन | 2.3 सेकंड | ~2.3 सेकंड |
ये नतीजे आशाजनक हैं. उपयोगकर्ताओं को काम का कॉन्टेंट तुरंत दिखता है, क्योंकि सर्वर साइड से रेंडर किया गया पेज पोस्ट लोड करने और दिखाने के लिए, अब JavaScript पर निर्भर नहीं करता.
फिर से हाइड्रेशन से बचना
याद है, मैंने कहा था कि "हमने क्लाइंट-साइड ऐप्लिकेशन के कोड में कोई बदलाव नहीं किया है"? यह झूठ है.
हमारा Express ऐप्लिकेशन, अनुरोध स्वीकार करता है. साथ ही, पेज को हेडलेस में लोड करने के लिए Puppeteer का इस्तेमाल करता है और नतीजे को जवाब के तौर पर दिखाता है. हालांकि, इस सेटअप में एक समस्या है.
जब उपयोगकर्ता का ब्राउज़र फ़्रंटएंड पर पेज लोड करता है, तो सर्वर पर वही JavaScript फिर से चलती है जो हेडलेस Chrome में चलती है. हमारे पास मार्कअप जनरेट करने के लिए दो जगहें हैं. #doublerender!
इसे ठीक करने के लिए, पेज को बताएं कि उसका एचटीएमएल पहले से मौजूद है.
इसका एक समाधान यह है कि पेज के JavaScript को यह जांचने के लिए कहा जाए कि <ul id="posts">
, लोड होने के समय पहले से ही DOM में है या नहीं. अगर ऐसा है, तो इसका मतलब है कि पेज को एसएसआर (सर्वर साइड रेंडरिंग) किया गया था. ऐसे में, आपको पोस्ट को फिर से जोड़ने की ज़रूरत नहीं है. 👍
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>
अनुकूलन
रेंडर किए गए नतीजों को कैश मेमोरी में सेव करने के अलावा, ssr()
में कई दिलचस्प ऑप्टिमाइज़ेशन किए जा सकते हैं. कुछ रणनीतियां तुरंत काम करती हैं, जबकि कुछ रणनीतियां ज़्यादा अनुमानित हो सकती हैं. आपको परफ़ॉर्मेंस से जुड़े जो फ़ायदे दिखते हैं वे इस बात पर निर्भर कर सकते हैं कि आपने किन पेजों को पहले से रेंडर किया है और ऐप्लिकेशन कितना जटिल है.
गैर-ज़रूरी अनुरोधों को रोकना
फ़िलहाल, पूरा पेज और उससे जुड़े सभी रिसॉर्स, बिना किसी शर्त के हेडलेस Chrome में लोड किए जाते हैं. हालांकि, हम सिर्फ़ दो बातों में दिलचस्पी रखते हैं:
- रेंडर किया गया मार्कअप.
- वे JS अनुरोध जिनसे वह मार्कअप जनरेट हुआ.
DOM न बनाने वाले नेटवर्क अनुरोध फ़ायदेमंद नहीं होते. इमेज, फ़ॉन्ट, स्टाइलशीट, और मीडिया जैसे रिसॉर्स, किसी पेज के एचटीएमएल को बनाने में हिस्सा नहीं लेते. ये किसी पेज के स्टाइल और स्ट्रक्चर को बेहतर बनाते हैं, लेकिन इसे सीधे तौर पर नहीं बनाते. हमें ब्राउज़र को इन संसाधनों को अनदेखा करने के लिए कहना चाहिए. इससे, हेडलेस Chrome के लिए वर्कलोड कम हो जाता है, बैंडविड्थ बचती है, और बड़े पेजों के लिए प्री-रेंडरिंग में लगने वाला समय कम हो सकता है.
DevTools प्रोटोकॉल, नेटवर्क इंटरसेप्शन नाम की एक बेहतरीन सुविधा के साथ काम करता है. इसका इस्तेमाल, ब्राउज़र से अनुरोध जारी होने से पहले उनमें बदलाव करने के लिए किया जा सकता है.
Puppeteer, नेटवर्क इंटरसेप्शन की सुविधा देता है. इसके लिए, page.setRequestInterception(true)
को चालू करें और पेज के request
इवेंट को सुनें.
इससे, हम कुछ संसाधनों के अनुरोधों को रोक सकते हैं और बाकी अनुरोधों को जारी रख सकते हैं.
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};
}
ज़रूरी रिसॉर्स को इनलाइन करना
आम तौर पर, किसी ऐप्लिकेशन को प्रोसेस करने और बिल्ड के समय पेज में ज़रूरी सीएसएस और JS को इनलाइन करने के लिए, अलग-अलग बिल्ड टूल (जैसे, gulp
) का इस्तेमाल किया जाता है. इससे फ़र्स्ट मीनिंगफ़ुल पेंट की प्रोसेस तेज़ हो सकती है, क्योंकि ब्राउज़र शुरुआती पेज लोड के दौरान कम अनुरोध करता है.
अलग से किसी बिल्ड टूल का इस्तेमाल करने के बजाय, ब्राउज़र को बिल्ड टूल के तौर पर इस्तेमाल करें! हम पेज के DOM, इनलाइन स्टाइल, JavaScript या पेज को प्री-रेंडर करने से पहले उसमें जोड़ने के लिए, Puppeteer का इस्तेमाल कर सकते हैं.
इस उदाहरण में, लोकल स्टाइलशीट के रिस्पॉन्स को इंटरसेप्ट करने और उन रिसॉर्स को पेज में <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};
}
सभी रेंडर में एक ही Chrome इंस्टेंस का फिर से इस्तेमाल करना
हर प्रीरेंडर के लिए नया ब्राउज़र लॉन्च करने से, बहुत ज़्यादा ओवरहेड होता है. इसके बजाय, एक इंस्टेंस लॉन्च किया जा सकता है और कई पेजों को रेंडर करने के लिए उसका फिर से इस्तेमाल किया जा सकता है.
Puppeteer, puppeteer.connect()
को कॉल करके और उसे इंस्टेंस के रिमोट डीबगिंग यूआरएल को पास करके, Chrome के किसी मौजूदा इंस्टेंस से फिर से कनेक्ट हो सकता है. लंबे समय तक चलने वाले ब्राउज़र इंस्टेंस को बनाए रखने के लिए, हम Chrome को लॉन्च करने वाले कोड को ssr()
फ़ंक्शन से एक्सप्रेस सर्वर में ले जा सकते हैं:
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};
}
उदाहरण: समय-समय पर प्री-रेंडर करने के लिए क्रॉन जॉब
एक साथ कई पेजों को रेंडर करने के लिए, शेयर किए गए ब्राउज़र इंस्टेंस का इस्तेमाल किया जा सकता है.
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!');
});
साथ ही, ssr.js में clearCache()
एक्सपोर्ट जोड़ें:
...
function clearCache() {
RENDER_CACHE.clear();
}
export {ssr, clearCache};
दूसरी ज़रूरी बातें
पेज के लिए सिग्नल बनाएं: "आपको हेडलेस मोड में रेंडर किया जा रहा है"
जब आपका पेज, सर्वर पर हेडलेस Chrome से रेंडर किया जा रहा हो, तो पेज के क्लाइंट-साइड लॉजिक के लिए यह जानना मददगार हो सकता है. मैंने अपने ऐप्लिकेशन में, इस हूक का इस्तेमाल अपने पेज के उन हिस्सों को "बंद करने" के लिए किया है जो पोस्ट मार्कअप को रेंडर करने में कोई भूमिका नहीं निभाते. उदाहरण के लिए, मैंने firebase-auth.js को धीरे-धीरे लोड करने वाले कोड को बंद कर दिया है. साइन इन करने के लिए कोई उपयोगकर्ता नहीं है!
पेज को हुक देने का आसान तरीका यह है कि रेंडर यूआरएल में ?headless
पैरामीटर जोड़ा जाए:
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};
}
और पेज पर, हम उस पैरामीटर को देख सकते हैं:
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>
Analytics में पेज व्यू की संख्या बढ़ाने से बचना
अगर आपकी साइट पर Analytics का इस्तेमाल किया जा रहा है, तो सावधानी बरतें. पेजों को पहले से रेंडर करने पर, पेज व्यू की संख्या बढ़ सकती है. खास तौर पर, आपको हिट की संख्या दोगुनी दिखेगी. पहला हिट तब होगा, जब बिना ग्राफ़िक यूज़र इंटरफ़ेस वाला Chrome ब्राउज़र पेज को रेंडर करेगा और दूसरा हिट तब होगा, जब उपयोगकर्ता का ब्राउज़र पेज को रेंडर करेगा.
तो इसे कैसे ठीक करें? 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();
});
अगर कोड कभी लोड नहीं होता है, तो पेज पर होने वाले हिट कभी रिकॉर्ड नहीं किए जाते. बूम 💥.
इसके अलावा, अपने सर्वर पर कितने प्री-रेंडर किए जा रहे हैं, इस बारे में अहम जानकारी पाने के लिए अपनी Analytics लाइब्रेरी लोड करना जारी रखें.
नतीजा
Puppeteer, आपके वेब सर्वर पर हेडलेस Chrome को साथी के तौर पर चलाकर, सर्वर साइड रेंडरिंग पेजों को आसान बनाता है. इस तरीके की मेरी पसंदीदा "सुविधा" यह है कि इससे कोड में कोई खास बदलाव किए बिना, अपने ऐप्लिकेशन की लोडिंग परफ़ॉर्मेंस और इंडेक्स करने की सुविधा को बेहतर बनाया जा सकता है!
अगर आपको यहां बताई गई तकनीकों का इस्तेमाल करने वाला कोई ऐप्लिकेशन देखना है, तो devwebfeed ऐप्लिकेशन देखें.
अन्य जानकारी
प्रायर आर्ट के बारे में चर्चा
क्लाइंट-साइड ऐप्लिकेशन को सर्वर साइड रेंडरिंग की सुविधा देना मुश्किल है. कितना मुश्किल है? देखें कि लोगों ने इस विषय के लिए कितने npm पैकेज लिखे हैं. एसएसआर (सर्वर साइड रेंडरिंग) वाले JS ऐप्लिकेशन बनाने में मदद करने के लिए, कई पैटर्न, टूल, और सेवाएं उपलब्ध हैं.
आइसोमरफ़िक / यूनिवर्सल JavaScript
यूनिवर्सल JavaScript के कॉन्सेप्ट का मतलब है: जो कोड सर्वर पर चलता है वही क्लाइंट (ब्राउज़र) पर भी चलता है. सर्वर और क्लाइंट के बीच कोड शेयर किया जाता है और सभी को एक शानदार अनुभव मिलता है.
Headless Chrome, सर्वर और क्लाइंट के बीच "आइसोमरफ़िक JS" को चालू करता है. अगर आपकी लाइब्रेरी, सर्वर (नोड) पर काम नहीं करती है, तो यह एक बेहतरीन विकल्प है.
कॉन्टेंट को पहले से रेंडर करने वाले टूल
Node कम्यूनिटी ने एसएसआर जेएस ऐप्लिकेशन के साथ काम करने के लिए, कई टूल बनाए हैं. इसमें कोई आश्चर्य नहीं है! मुझे लगता है कि इनमें से कुछ टूल के लिए, आपके अनुभव के हिसाब से नतीजे मिल सकते हैं. इसलिए, किसी टूल को इस्तेमाल करने से पहले, उसकी पूरी जानकारी ज़रूर हासिल कर लें. उदाहरण के लिए, कुछ एसएसआर टूल पुराने हैं और वे बिना ग्राफ़िक यूज़र इंटरफ़ेस वाले Chrome (या किसी भी बिना ग्राफ़िक यूज़र इंटरफ़ेस वाले ब्राउज़र) का इस्तेमाल नहीं करते. इसके बजाय, वे PhantomJS (जिसे पुराना Safari भी कहा जाता है) का इस्तेमाल करते हैं. इसका मतलब है कि अगर आपके पेजों पर नई सुविधाएं इस्तेमाल की जा रही हैं, तो वे ठीक से रेंडर नहीं होंगे.
प्रीरेंडर एक अहम अपवाद है. Prerender का इस्तेमाल करना दिलचस्प है, क्योंकि यह बिना ग्राफ़िक यूज़र इंटरफ़ेस वाले Chrome का इस्तेमाल करता है. साथ ही, इसमें Express के लिए ड्रॉप-इन मिडलवेयर भी शामिल है:
const prerender = require('prerender');
const server = prerender();
server.use(prerender.removeScriptTags());
server.use(prerender.blockResources());
server.start();
ध्यान दें कि प्रीरेंडर की सुविधा, अलग-अलग प्लैटफ़ॉर्म पर Chrome डाउनलोड करने और इंस्टॉल करने की जानकारी नहीं दिखाती. अक्सर, इसे सही तरीके से लागू करना काफ़ी मुश्किल होता है. यही वजह है कि Puppeteer आपके लिए यह काम करता है.