হেডলেস ক্রোম: সার্ভার-সাইড রেন্ডারিং JS সাইটগুলির একটি উত্তর৷

এক্সপ্রেস ওয়েব সার্ভারে সার্ভার-সাইড রেন্ডারিং (SSR) ক্ষমতা যুক্ত করতে আপনি কিভাবে Puppeteer API ব্যবহার করতে পারেন তা জানুন। সবচেয়ে ভালো অংশ হল আপনার অ্যাপের কোডে খুব ছোট পরিবর্তন প্রয়োজন। হেডলেস সব ভারী উত্তোলন করে।

কোডের কয়েকটি লাইনে আপনি যেকোনো পৃষ্ঠা SSR করতে পারেন এবং এর চূড়ান্ত মার্কআপ পেতে পারেন।

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

কেন হেডলেস ক্রোম ব্যবহার করবেন?

আপনি হেডলেস ক্রোমে আগ্রহী হতে পারেন যদি:

কিছু ফ্রেমওয়ার্ক যেমন সার্ভার-সাইড রেন্ডারিংকে সম্বোধন করে এমন সরঞ্জামগুলির সাথে প্রেক্ট শিপ । আপনার ফ্রেমওয়ার্কের একটি প্রি-রেন্ডারিং সমাধান থাকলে, আপনার ওয়ার্কফ্লোতে পাপেটিয়ার এবং হেডলেস ক্রোম আনার পরিবর্তে এটির সাথে লেগে থাকুন।

আধুনিক ওয়েব ক্রল করা

সার্চ ইঞ্জিন ক্রলার, সোশ্যাল শেয়ারিং প্ল্যাটফর্ম, এমনকি ব্রাউজারগুলি ঐতিহাসিকভাবে ওয়েব এবং পৃষ্ঠের বিষয়বস্তু সূচী করার জন্য স্ট্যাটিক HTML মার্কআপের উপর একচেটিয়াভাবে নির্ভর করে। আধুনিক ওয়েব অনেক ভিন্ন কিছুতে বিকশিত হয়েছে। JavaScript-ভিত্তিক অ্যাপ্লিকেশনগুলি এখানে থাকার জন্য রয়েছে, যার মানে হল যে অনেক ক্ষেত্রে, আমাদের সামগ্রী ক্রলিং সরঞ্জামগুলিতে অদৃশ্য হতে পারে৷

Googlebot, আমাদের অনুসন্ধান ক্রলার, জাভাস্ক্রিপ্ট প্রক্রিয়া করে এবং নিশ্চিত করে যে এটি সাইট পরিদর্শনকারী ব্যবহারকারীদের অভিজ্ঞতাকে অবনমিত করে না। কিছু পার্থক্য এবং সীমাবদ্ধতা রয়েছে যা ক্রলাররা কীভাবে আপনার সামগ্রী অ্যাক্সেস এবং রেন্ডার করে তা সামঞ্জস্য করার জন্য আপনার পৃষ্ঠা এবং অ্যাপ্লিকেশন ডিজাইন করার সময় আপনাকে বিবেচনা করতে হবে।

প্রি-রেন্ডার পেজ

সমস্ত ক্রলার HTML বোঝে। ক্রলাররা জাভাস্ক্রিপ্ট ইন্ডেক্স করতে পারে তা নিশ্চিত করতে, আমাদের একটি টুল দরকার যা:

  • কিভাবে সব ধরনের আধুনিক জাভাস্ক্রিপ্ট চালাতে হয় এবং স্ট্যাটিক HTML তৈরি করতে হয় তা জানে।
  • ওয়েব বৈশিষ্ট্য যোগ করার সাথে সাথে আপ টু ডেট থাকে।
  • আপনার অ্যাপ্লিকেশনে সামান্য থেকে কোন কোড আপডেট ছাড়াই চলে।

ভালো লাগছে তাই না? সেই টুলটি হল ব্রাউজার ! হেডলেস ক্রোম আপনি কোন লাইব্রেরি, ফ্রেমওয়ার্ক, বা টুল চেইন ব্যবহার করেন তা বিবেচনা করে না।

উদাহরণস্বরূপ, যদি আপনার অ্যাপ্লিকেশন Node.js দিয়ে তৈরি করা হয়, তাহলে Puppeteer হল 0.headless Chrome-এর সাথে কাজ করার একটি সহজ উপায়।

চলুন শুরু করা যাক একটি ডাইনামিক পেজ যা জাভাস্ক্রিপ্ট দিয়ে HTML তৈরি করে:

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.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};

প্রধান পরিবর্তন:

  • ক্যাশিং যোগ করা হয়েছে। রেন্ডার করা এইচটিএমএল ক্যাশ করা হল প্রতিক্রিয়ার সময় দ্রুত করার সবচেয়ে বড় জয়। যখন পৃষ্ঠাটি পুনরায় অনুরোধ করা হয়, আপনি সম্পূর্ণরূপে হেডলেস ক্রোম চালানো এড়িয়ে যান। আমি পরে অন্যান্য অপ্টিমাইজেশান নিয়ে আলোচনা করব।
  • পৃষ্ঠা লোড করার সময় শেষ হলে মৌলিক ত্রুটি পরিচালনা যোগ করুন।
  • page.waitForSelector('#posts') এ একটি কল যোগ করুন। এটি নিশ্চিত করে যে আমরা সিরিয়ালাইজড পৃষ্ঠাটি ডাম্প করার আগে পোস্টগুলি DOM-এ বিদ্যমান।
  • বিজ্ঞান যোগ করুন। পৃষ্ঠাটি রেন্ডার করতে কতক্ষণ হেডলেস লাগে তা লগ করুন এবং HTML সহ রেন্ডারিং সময় ফেরত দিন৷
  • ssr.mjs নামের একটি মডিউলে কোডটি আটকে দিন।

উদাহরণ ওয়েব সার্ভার

অবশেষে, এখানে একটি ছোট এক্সপ্রেস সার্ভার যা এটিকে একত্রিত করে। প্রধান হ্যান্ডলার URL টি 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>

নতুন সার্ভার-টাইমিং API-এর জন্য একটি নিখুঁত ব্যবহারের ক্ষেত্রে

সার্ভার-টাইমিং এপিআই সার্ভারের কর্মক্ষমতা মেট্রিক্স (যেমন অনুরোধ এবং প্রতিক্রিয়ার সময় বা ডাটাবেস লুকআপ) ব্রাউজারে ফেরত পাঠায়। ক্লায়েন্ট কোড একটি ওয়েব অ্যাপের সামগ্রিক কর্মক্ষমতা ট্র্যাক করতে এই তথ্য ব্যবহার করতে পারে।

সার্ভার-টাইমিংয়ের জন্য একটি নিখুঁত ব্যবহারের ক্ষেত্রে একটি পৃষ্ঠা প্রি-রেন্ডার করতে হেডলেস ক্রোমের কতক্ষণ সময় লাগে তা রিপোর্ট করা! এটি করতে, সার্ভার প্রতিক্রিয়াতে Server-Timing শিরোনাম যোগ করুন:

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

ক্লায়েন্টে, পারফরমেন্স এপিআই এবং পারফরম্যান্স অবজারভার এই মেট্রিক্সগুলি অ্যাক্সেস করতে ব্যবহার করা যেতে পারে:

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)"
}

কর্মক্ষমতা ফলাফল

নিম্নলিখিত ফলাফলগুলি পরে আলোচনা করা বেশিরভাগ কর্মক্ষমতা অপ্টিমাইজেশানগুলিকে অন্তর্ভুক্ত করে৷

আমার একটি অ্যাপে ( কোড ), হেডলেস ক্রোম সার্ভারে পৃষ্ঠাটি রেন্ডার করতে প্রায় 1 সেকেন্ড সময় নেয়। একবার পৃষ্ঠাটি ক্যাশে করা হলে, DevTools 3G স্লো এমুলেশন FCP কে ক্লায়েন্ট-সাইড সংস্করণের চেয়ে 8.37 সেকেন্ড দ্রুত রাখে।

প্রথম পেইন্ট (FP) ফার্স্ট কনটেন্টফুল পেইন্ট (FCP)
ক্লায়েন্ট-সাইড অ্যাপ 4s 11 সে
এসএসআর সংস্করণ 2.3s ~2.3 সেকেন্ড

এই ফলাফলগুলি আশাব্যঞ্জক। ব্যবহারকারীরা অর্থপূর্ণ বিষয়বস্তু আরও দ্রুত দেখতে পান কারণ সার্ভার-সাইড রেন্ডার করা পৃষ্ঠাটি আর পোস্টগুলি লোড করার জন্য জাভাস্ক্রিপ্টের উপর নির্ভর করে না

রি-হাইড্রেশন প্রতিরোধ করা

মনে আছে যখন আমি বলেছিলাম "আমরা ক্লায়েন্ট-সাইড অ্যাপে কোনো কোড পরিবর্তন করিনি"? এটা মিথ্যা ছিল.

আমাদের এক্সপ্রেস অ্যাপটি একটি অনুরোধ নেয়, পৃষ্ঠাটিকে হেডলেস লোড করতে Puppeteer ব্যবহার করে এবং প্রতিক্রিয়া হিসাবে ফলাফলটি পরিবেশন করে। কিন্তু এই সেটআপে সমস্যা আছে।

ব্যবহারকারীর ব্রাউজার ফ্রন্টএন্ডে পৃষ্ঠা লোড করলে সার্ভারে হেডলেস ক্রোমে চালানো একই JS আবার চলে । আমরা মার্কআপ উৎপন্ন দুটি জায়গা আছে. #ডবলরেন্ডার !

এটা ঠিক করা যাক. আমাদের পৃষ্ঠাটিকে বলতে হবে এর HTML ইতিমধ্যেই রয়েছে৷ আমি যে সমাধানটি খুঁজে পেয়েছি তা হল পৃষ্ঠা JS চেক করা যে <ul id="posts"> লোডের সময় ইতিমধ্যেই DOM-এ আছে কিনা। যদি তা হয়, আমরা জানি পৃষ্ঠাটি SSR'd ছিল এবং আবার পোস্টগুলি পুনরায় যোগ করা এড়াতে পারি৷ 👍

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() করতে পারি। কিছু দ্রুত জয় হয় যখন অন্যরা আরও অনুমানমূলক হতে পারে। আপনি যে পারফরম্যান্স সুবিধাগুলি দেখতে পান তা শেষ পর্যন্ত আপনার প্রি-রেন্ডার করা পৃষ্ঠাগুলির প্রকার এবং অ্যাপের জটিলতার উপর নির্ভর করতে পারে।

অপ্রয়োজনীয় অনুরোধ বাতিল করুন

এই মুহূর্তে, সম্পূর্ণ পৃষ্ঠা (এবং এটির অনুরোধ করা সমস্ত সংস্থান) নিঃশর্তভাবে হেডলেস ক্রোমে লোড করা হয়েছে৷ যাইহোক, আমরা শুধুমাত্র দুটি বিষয়ে আগ্রহী:

  1. রেন্ডার করা মার্কআপ।
  2. JS অনুরোধ যে মার্কআপ উত্পাদিত.

নেটওয়ার্ক অনুরোধগুলি যেগুলি DOM তৈরি করে না সেগুলি অযথা ৷ ছবি, ফন্ট, স্টাইলশীট এবং মিডিয়ার মতো সংস্থানগুলি একটি পৃষ্ঠার HTML তৈরিতে অংশগ্রহণ করে না। তারা একটি পৃষ্ঠার গঠন শৈলী এবং পরিপূরক করে কিন্তু তারা স্পষ্টভাবে এটি তৈরি করে না। আমাদের ব্রাউজারকে বলা উচিত এই সম্পদগুলিকে উপেক্ষা করতে। এটি হেডলেস ক্রোমের জন্য কাজের চাপ কমায়, ব্যান্ডউইথ সংরক্ষণ করে এবং বড় পৃষ্ঠাগুলির জন্য প্রি-রেন্ডারিং সময়কে দ্রুততর করে।

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

ইনলাইন সমালোচনামূলক সম্পদ

একটি অ্যাপ প্রসেস করার জন্য আলাদা বিল্ড টুল (যেমন gulp ) ব্যবহার করা এবং বিল্ড-টাইমে পেজে ক্রিটিকাল CSS এবং JS ইনলাইন করা সাধারণ। এটি প্রথম অর্থপূর্ণ পেইন্টের গতি বাড়াতে পারে কারণ প্রাথমিক পৃষ্ঠা লোডের সময় ব্রাউজার কম অনুরোধ করে।

একটি পৃথক বিল্ড টুলের পরিবর্তে, আপনার বিল্ড টুল হিসাবে ব্রাউজার ব্যবহার করুন ! আমরা পৃষ্ঠার DOM, ইনলাইনিং স্টাইল, জাভাস্ক্রিপ্ট, বা অন্য যা কিছু প্রি-রেন্ডার করার আগে পৃষ্ঠায় আটকে রাখতে চান তা পরিচালনা করতে আমরা 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:

  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.connect() কল করে এবং ইন্সট্যান্সের রিমোট ডিবাগিং ইউআরএল পাস করে 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};

অন্যান্য বিবেচ্য বিষয়

পৃষ্ঠার জন্য একটি সংকেত তৈরি করুন: "আপনাকে হেডলেস রেন্ডার করা হচ্ছে"

যখন আপনার পৃষ্ঠাটি সার্ভারে হেডলেস ক্রোম দ্বারা রেন্ডার করা হচ্ছে, তখন এটি পৃষ্ঠার ক্লায়েন্ট-সাইড লজিকের জন্য এটি জানতে সহায়ক হতে পারে৷ আমার অ্যাপে, আমি এই হুকটি আমার পৃষ্ঠার অংশগুলিকে "বন্ধ" করতে ব্যবহার করেছি যা পোস্ট মার্কআপ রেন্ডার করার ক্ষেত্রে ভূমিকা পালন করে না। উদাহরণস্বরূপ, আমি অক্ষম কোড অক্ষম করেছি যা 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>

অ্যানালিটিক্স পেজভিউ বৃদ্ধি করা এড়িয়ে চলুন

আপনি যদি আপনার সাইটে অ্যানালিটিক্স ব্যবহার করেন তবে সতর্ক থাকুন। প্রি-রেন্ডারিং পেজ ইনফ্ল্যাটেড পেজভিউ হতে পারে। বিশেষত, আপনি হিটের সংখ্যা 2x দেখতে পাবেন , একটি হিট যখন হেডলেস ক্রোম পৃষ্ঠাটি রেন্ডার করে এবং অন্যটি যখন ব্যবহারকারীর ব্রাউজার এটিকে রেন্ডার করে।

তাই ঠিক কি? অ্যানালিটিক্স লাইব্রেরি লোড করার চেষ্টা করে এমন যেকোনো অনুরোধ (গুলি) বাতিল করতে নেটওয়ার্ক ইন্টারসেপশন ব্যবহার করুন।

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

কোড লোড না হলে পৃষ্ঠা হিট কখনই রেকর্ড করা হয় না। বুম 💥।

বিকল্পভাবে, আপনার সার্ভার কতগুলি প্রি-রেন্ডার করছে তার অন্তর্দৃষ্টি পেতে আপনার অ্যানালিটিক্স লাইব্রেরিগুলি লোড করা চালিয়ে যান।

উপসংহার

Puppeteer আপনার ওয়েব সার্ভারে সঙ্গী হিসাবে হেডলেস ক্রোম চালানোর মাধ্যমে সার্ভার-সাইড রেন্ডার পৃষ্ঠাগুলিকে সহজ করে তোলে৷ এই পদ্ধতির আমার প্রিয় "বৈশিষ্ট্য" হল যে আপনি উল্লেখযোগ্য কোড পরিবর্তন ছাড়াই লোডিং কর্মক্ষমতা এবং আপনার অ্যাপের সূচকযোগ্যতা উন্নত করেন!

আপনি যদি এখানে বর্ণিত কৌশলগুলি ব্যবহার করে এমন একটি কার্যকরী অ্যাপ দেখতে আগ্রহী হন, তাহলে devwebfeed অ্যাপটি দেখুন।

পরিশিষ্ট

পূর্বের শিল্পের আলোচনা

সার্ভার-সাইড রেন্ডারিং ক্লায়েন্ট-সাইড অ্যাপস কঠিন। কিভাবে হার্ড? শুধু দেখুন কতগুলি এনপিএম প্যাকেজ লোকেরা লিখেছেন যা বিষয়ের জন্য উত্সর্গীকৃত। SSRing JS অ্যাপের সাহায্যে অগণিত প্যাটার্ন , টুল এবং পরিষেবা উপলব্ধ রয়েছে।

আইসোমরফিক / ইউনিভার্সাল জাভাস্ক্রিপ্ট

ইউনিভার্সাল জাভাস্ক্রিপ্টের ধারণার অর্থ: সার্ভারে যে কোডটি চলে সেই একই কোড ক্লায়েন্টেও (ব্রাউজার) চলে। আপনি সার্ভার এবং ক্লায়েন্টের মধ্যে কোড ভাগ করেন এবং প্রত্যেকে জেনের একটি মুহূর্ত অনুভব করে।

হেডলেস ক্রোম সার্ভার এবং ক্লায়েন্টের মধ্যে "আইসোমরফিক জেএস" সক্ষম করে। আপনার লাইব্রেরি সার্ভারে (নোড) কাজ না করলে এটি একটি দুর্দান্ত বিকল্প।

প্রি-রেন্ডার টুল

নোড সম্প্রদায় এসএসআর জেএস অ্যাপগুলির সাথে কাজ করার জন্য প্রচুর সরঞ্জাম তৈরি করেছে। সেখানে কোন চমক! ব্যক্তিগতভাবে, আমি এই সরঞ্জামগুলির কিছু সহYMMV খুঁজে পেয়েছি, তাই একটি প্রতিশ্রুতি দেওয়ার আগে অবশ্যই আপনার হোমওয়ার্ক করুন। উদাহরণস্বরূপ, কিছু SSR টুল পুরানো এবং হেডলেস ক্রোম ব্যবহার করে না (অথবা সেই বিষয়ে কোনো হেডলেস ব্রাউজার)। পরিবর্তে, তারা ফ্যান্টমজেএস (ওরফে পুরানো সাফারি) ব্যবহার করে, যার অর্থ আপনার পৃষ্ঠাগুলি সঠিকভাবে রেন্ডার হবে না যদি তারা নতুন বৈশিষ্ট্যগুলি ব্যবহার করে।

উল্লেখযোগ্য ব্যতিক্রমগুলির মধ্যে একটি হল প্রিরেন্ডার । প্রিরেন্ডার আকর্ষণীয় যে এটি হেডলেস ক্রোম ব্যবহার করে এবং এক্সপ্রেসের জন্য ড্রপ-ইন মিডলওয়্যারের সাথে আসে:

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

এটি লক্ষণীয় যে প্রিরেন্ডার বিভিন্ন প্ল্যাটফর্মে ক্রোম ডাউনলোড এবং ইনস্টল করার বিবরণ ছেড়ে দেয়। প্রায়শই, এটি সঠিক হওয়া মোটামুটি চতুর , যা Puppeteer আপনার জন্য করার একটি কারণ। আমার কিছু অ্যাপ রেন্ডার করার অনলাইন পরিষেবাতেও আমার সমস্যা হয়েছে:

একটি ব্রাউজারে রেন্ডার করা ক্রোমেস্ট্যাটাস
একটি ব্রাউজারে রেন্ডার করা সাইট
ক্রোমেস্ট্যাটাস প্রি-রেন্ডার দ্বারা রেন্ডার করা হয়েছে
একই সাইট prerender.io দ্বারা রেন্ডার করা হয়েছে