बिना ग्राफ़िक यूज़र इंटरफ़ेस वाला Chrome: JS साइटों को सर्वर साइड से रेंडर करने से जुड़ा जवाब

जानें कि किसी एक्सप्रेस वेब सर्वर में सर्वर-साइड रेंडरिंग (SSR) क्षमताओं को जोड़ने के लिए, Puppeteer API का इस्तेमाल कैसे किया जा सकता है. सबसे अच्छी बात यह है कि आपके ऐप्लिकेशन के लिए कोड में बहुत मामूली बदलाव करने पड़ते हैं. बिना ग्राफ़िक यूज़र इंटरफ़ेस वाले सभी मुश्किल काम भी कर देता है.

कोड की कुछ लाइनों में किसी भी पेज के लिए एसएसआर किया जा सकता है और उसका फ़ाइनल मार्कअप पाया जा सकता है.

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 का इस्तेमाल क्यों करना चाहिए?

बिना ग्राफ़िक यूज़र इंटरफ़ेस वाले Chrome का इस्तेमाल करने में आपकी दिलचस्पी हो सकती है, अगर:

Preact जैसे कुछ फ़्रेमवर्क, टूल के साथ शिप किए जाते हैं. ये फ़्रेमवर्क सर्वर साइड रेंडरिंग से जुड़ी समस्या को हल करते हैं. अगर आपके फ़्रेमवर्क में प्रीरेंडरिंग की सुविधा है, तो वर्कफ़्लो में Puppeteer और हेडलेस Chrome को शामिल करने के बजाय उस पर बने रहें.

आधुनिक वेब को क्रॉल करना

वेब को इंडेक्स करने और कॉन्टेंट को दिखाने के लिए, पहले से ही सर्च इंजन के क्रॉलर, सोशल शेयरिंग प्लैटफ़ॉर्म, यहां तक कि ब्राउज़र भी खास तौर पर स्टैटिक एचटीएमएल मार्कअप पर निर्भर रहे हैं. आधुनिक वेब का विकास काफ़ी कुछ अलग हो गया है. JavaScript पर आधारित ऐप्लिकेशन आने वाले हैं. इसका मतलब है कि कई मामलों में, हमारा कॉन्टेंट क्रॉल करने वाले टूल पर नहीं दिख सकता.

हमारा Search क्रॉलर Googlebot, JavaScript को प्रोसेस करता है. साथ ही, यह पक्का करता है कि यह साइट पर आने वाले लोगों के अनुभव को खराब न करे. अपने पेज और ऐप्लिकेशन डिज़ाइन करते समय, आपको कुछ अंतरों और सीमाओं का ध्यान रखना होगा. इससे आपको अपने कॉन्टेंट को ऐक्सेस और रेंडर करने के तरीके के बारे में जानकारी मिल पाएगी.

पेजों को प्रीरेंडर करना

सभी क्रॉलर को एचटीएमएल के बारे में जानकारी होती है. यह पक्का करने के लिए कि क्रॉलर JavaScript को इंडेक्स कर पाएं, हमें एक ऐसे टूल की ज़रूरत है जो:

  • यह जानता है कि सभी तरह के आधुनिक JavaScript को कैसे चलाना है और स्टैटिक एचटीएमएल जनरेट करना है.
  • वेब में नई सुविधाएं जोड़ने पर यह अप-टू-डेट रहती है.
  • आपके ऐप्लिकेशन के लिए कम या बिना कोड अपडेट के काम करता है.

है न? वह टूल है ब्राउज़र! बिना ग्राफ़िक यूज़र इंटरफ़ेस वाले Chrome का इस्तेमाल इस बात से नहीं किया जाता कि आपने किस लाइब्रेरी, फ़्रेमवर्क या टूल चेन का इस्तेमाल किया है.

उदाहरण के लिए, अगर आपका ऐप्लिकेशन Node.js की मदद से बनाया गया है, तो Puppeteer इस्तेमाल करके 0.headless 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') को एक कॉल जोड़ें. इससे यह पक्का होता है कि क्रम से लगाए गए पेज को डंप करने से पहले पोस्ट, डीओएम में मौजूद हैं.
  • विज्ञान जोड़ें. लॉग करें कि पेज को रेंडर करने में हेडलेस कितना समय लगता है. इसके बाद, एचटीएमएल के साथ रेंडर होने में लगने वाला समय देखें.
  • 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 एपीआई, सर्वर परफ़ॉर्मेंस मेट्रिक (जैसे कि अनुरोध और जवाब देने का समय या डेटाबेस लुकअप) को ब्राउज़र पर वापस भेजता है. क्लाइंट कोड इस जानकारी का उपयोग किसी वेब ऐप्लिकेशन के पूरे प्रदर्शन को ट्रैक करने के लिए कर सकता है.

Server-Timing के इस्तेमाल का सबसे सही उदाहरण यह है कि बिना ग्राफ़िक यूज़र इंटरफ़ेस वाले Chrome को किसी पेज को प्रीरेंडर करने में कितना समय लगता है! ऐसा करने के लिए, सर्वर रिस्पॉन्स में Server-Timing हेडर जोड़ें:

res.set('Server-Timing', `Prerender;dur=1000;desc="Headless render time (ms)"`);

क्लाइंट पर, Performance API और 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)"
}

परफ़ॉर्मेंस के नतीजे

इन नतीजों में, परफ़ॉर्मेंस के ज़्यादातर ऐसे ऑप्टिमाइज़ेशन शामिल हैं जिनके बारे में बाद में चर्चा की गई थी.

मेरे किसी ऐप्लिकेशन (कोड) पर, बिना ग्राफ़िक यूज़र इंटरफ़ेस के Chrome को सर्वर पर पेज को रेंडर करने में करीब एक सेकंड लगता है. पेज के कैश मेमोरी में सेव होने के बाद, DevTools 3G स्लो एम्युलेशन, क्लाइंट-साइड वर्शन के मुकाबले एफ़सीपी को 8.37 सेकंड तेज़ी पर सेट करता है.

फ़र्स्ट पेंट (FP)First Contentful Paint (FCP)
क्लाइंट-साइड ऐप्लिकेशन4 सेकंड 11 सेकंड
SSR वर्शन2.3 सेकंड~2.3 सेकंड

ये नतीजे भरोसेमंद हैं. उपयोगकर्ताओं को काम का कॉन्टेंट ज़्यादा जल्दी दिखता है, क्योंकि सर्वर साइड से रेंडर किया गया पेज, पोस्ट लोड करने और दिखाने के लिए JavaScript पर निर्भर नहीं रहता.

शरीर में पानी की कमी पूरी करना

क्या मुझे याद है जब मैंने कहा था, "हमने क्लाइंट-साइड ऐप्लिकेशन के कोड में कोई बदलाव नहीं किया"? यह झूठ था.

हमारा एक्सप्रेस ऐप्लिकेशन अनुरोध स्वीकार करता है, पेज को बिना ग्राफ़िक यूज़र इंटरफ़ेस के पेज पर लोड करने के लिए Puppeteer का इस्तेमाल करता है और रिस्पॉन्स के तौर पर नतीजे दिखाता है. लेकिन इस सेटअप में एक समस्या है.

सर्वर पर बिना ग्राफ़िक यूज़र इंटरफ़ेस वाले Chrome में काम करने वाला JS फिर से चलता है. ऐसा तब होता है, जब उपयोगकर्ता का ब्राउज़र फ़्रंटएंड पर पेज को लोड करता है. हमारे पास दो जगहें हैं, जो मार्कअप जनरेट कर रही हैं. #doudlerender!

चलिए, इसे ठीक करते हैं. हमें पेज को बताना होगा कि इसका एचटीएमएल पहले से ही मौजूद है. इसका समाधान यह है कि पेज JS की जांच करें, क्योंकि लोड होने के समय DOM में <ul id="posts"> पहले से मौजूद है. अगर ऐसा है, तो हमें पता है कि पेज एसएसआर का था और हम पोस्ट को फिर से जोड़ने से बच सकते हैं. 👍

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 में लोड हो जाता है. हालांकि, हमारी सिर्फ़ दो चीज़ों में दिलचस्पी है:

  1. रेंडर किया गया मार्कअप.
  2. वह JS अनुरोध जिसने वह मार्कअप बनाया.

ऐसे नेटवर्क अनुरोध जो डीओएम नहीं बनाते हैं, वे खराब होते हैं. इमेज, फ़ॉन्ट, स्टाइलशीट, और मीडिया जैसे रिसॉर्स, पेज का एचटीएमएल बनाने में मदद नहीं करते. वे पेज के स्ट्रक्चर को स्टाइल देते हैं और उसे बेहतर बनाते हैं. हालांकि, पेज को ऐसा नहीं बनाया जा सकता. हमें ब्राउज़र को इन संसाधनों को अनदेखा करने के लिए कहना चाहिए. इससे बिना ग्राफ़िक यूज़र इंटरफ़ेस वाले Chrome पर वर्कलोड कम हो जाता है और बैंडविड्थ कम हो जाती है. साथ ही, बड़े पेजों के लिए प्रीरेंडरिंग में लगने वाला समय बढ़ जाता है.

DevTool प्रोटोकॉल, नेटवर्क इंटरसेप्शन नाम की एक बेहतरीन सुविधा के साथ काम करता है. इसका इस्तेमाल, ब्राउज़र की ओर से अनुरोधों को जारी करने से पहले उनमें बदलाव करने के लिए किया जा सकता है. 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 में हेर-फेर करने के लिए, Puppeteer का इस्तेमाल कर सकते हैं. इसकी मदद से, लाइनिंग स्टाइल, JavaScript या ऐसी किसी भी चीज़ में बदलाव किया जा सकता है जिसे पेज को पहले से रेंडर करने से पहले रखना है.

इस उदाहरण में, लोकल स्टाइलशीट के लिए रिस्पॉन्स को इंटरसेप्ट करने और उन रिसॉर्स को पेज में <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:

  1. Use a page.on('response') handler to listen for network responses.
  2. Stashes the responses of local stylesheets.
  3. Finds all <link rel="stylesheet"> in the DOM and replaces them with an equivalent <style>. See page.$$eval API docs. The style.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};
}

उदाहरण: क्रॉन जॉब से लेकर समय-समय पर प्रीरेंडरिंग तक

अपने App Engine डैशबोर्ड ऐप्लिकेशन में, मैंने एक क्रॉन हैंडलर सेट अप किया है, ताकि साइट के टॉप पेजों को समय-समय पर फिर से रेंडर किया जा सके. इससे वेबसाइट पर आने वाले लोगों को हमेशा तेज़ और नया कॉन्टेंट देखने में मदद मिलती है. साथ ही, उन्हें नई प्रीरेंडरिंग की "स्टार्टअप लागत" का पता लगाने में भी मदद मिलती है. इस मामले में Chrome के कई इंस्टेंस देना खराब होगा. इसके बजाय, मैं कई पेजों को एक साथ रेंडर करने के लिए शेयर किए गए ब्राउज़र इंस्टेंस का इस्तेमाल कर रहा/रही हूं:

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();
});

अगर कोड कभी लोड नहीं होता है, तो पेज हिट कभी रिकॉर्ड नहीं होते. बूम \n.

इसके अलावा, Analytics लाइब्रेरी को लोड करना जारी रखें, ताकि यह पता लगाया जा सके कि आपका सर्वर कितनी प्रीरेंडरिंग कर रहा है.

नतीजा

Puppeteer आपके वेब सर्वर पर, एक साथी के रूप में बिना ग्राफ़िक यूज़र इंटरफ़ेस वाले Chrome को चलाकर सर्वर-साइड पर पेजों को रेंडर करना आसान बनाता है. इस तरीके की मेरी सबसे पसंदीदा "सुविधा" यह है कि इससे कोड में ज़रूरी बदलाव किए बिना अपने ऐप्लिकेशन की लोडिंग परफ़ॉर्मेंस को बेहतर बनाने और इंडेक्स करने की क्षमता को बेहतर बनाने में मदद मिलती है!

अगर आप कोई ऐसा ऐप्लिकेशन देखना चाहते हैं जो यहां बताई गई तकनीकों का इस्तेमाल करता है, तो devwebfeed ऐप्लिकेशन देखें.

अन्य जानकारी

पुराने आर्ट की चर्चा

सर्वर-साइड रेंडरिंग क्लाइंट-साइड ऐप्लिकेशन मुश्किल होता है. कितना मुश्किल है? देखें कि लोगों ने इस विषय के लिए कितने एनपीएम पैकेज लिखे हैं. SSRing JS ऐप्लिकेशन में मदद के लिए अनगिनत पैटर्न, tools, और सेवाएं उपलब्ध हैं.

आइसोमॉर्फ़िक / यूनिवर्सल JavaScript

यूनिवर्सल JavaScript के कॉन्सेप्ट का मतलब है: सर्वर पर चलने वाला कोड, क्लाइंट (ब्राउज़र) पर भी चलता है. सर्वर और क्लाइंट के बीच कोड शेयर किया जाता है और सभी को अच्छा महसूस होता है.

बिना ग्राफ़िक यूज़र इंटरफ़ेस वाला Chrome, सर्वर और क्लाइंट के बीच "आइसोमॉर्फ़िक जेएस" को चालू करता है. अगर आपकी लाइब्रेरी सर्वर (नोड) पर काम नहीं करती है, तो यह एक अच्छा विकल्प है.

प्रीरेंडर टूल

नोड समुदाय ने SSR JS ऐप्लिकेशन से निपटने के लिए कई टूल बनाए हैं. कोई बात नहीं! निजी तौर पर मुझे पता चला है कि YMMV इनमें से कुछ टूल के साथ काम करता है, इसलिए कोई भी विकल्प चुनने से पहले, पक्का कर लें कि आपने अपनी तैयारी पूरी कर ली है. उदाहरण के लिए, कुछ SSR टूल पुराने हैं और बिना ग्राफ़िक यूज़र इंटरफ़ेस वाले Chrome या इस मामले में किसी भी बिना ग्राफ़िक यूज़र इंटरफ़ेस वाले ब्राउज़र का इस्तेमाल नहीं करते हैं. इसके बजाय, वे PhantomJS (यानी पुराने Safari) का इस्तेमाल करते हैं, जिसका मतलब है कि नई सुविधाओं का इस्तेमाल करने पर आपके पेज ठीक से रेंडर नहीं होंगे.

प्रीरेंडर ध्यान देने लायक अपवादों में से एक है. प्रीरेंडरिंग की सुविधा इस बात में दिलचस्प है कि वह बिना ग्राफ़िक यूज़र इंटरफ़ेस वाले Chrome का इस्तेमाल करता है और उसमें ड्रॉप-इन एक्सप्रेशन के लिए मिडलवेयर की सुविधा भी होती है:

const prerender = require('prerender');
const server = prerender();
server.use(prerender.removeScriptTags());
server.use(prerender.blockResources());
server.start();

इस बात पर ध्यान देना ज़रूरी है कि प्रीरेंडरिंग से आपको अलग-अलग प्लैटफ़ॉर्म पर Chrome को डाउनलोड और इंस्टॉल करने की जानकारी नहीं मिलती. कई बार सही जवाब देना मुश्किल होता है. इसी वजह से Puppeteerआपके लिए ऐसा करता है. मुझे अपने कुछ ऐप्लिकेशन को रेंडर करने में ऑनलाइन सेवा में भी समस्याएं आई थीं:

ब्राउज़र में रेंडर की गई chromestatus
ब्राउज़र में रेंडर की गई साइट
प्रीरेंडरिंग की मदद से रेंडर किया गया chromestatus
preorder.io की मदद से, उसी साइट को रेंडर किया गया