Headless Chrome: پاسخی به سایت های جاوا اسکریپت رندر سمت سرور

آدی عثمانی
Addy Osmani

بیاموزید که چگونه می توانید از API های Puppeteer برای افزودن قابلیت های رندر سمت سرور (SSR) به وب سرور Express استفاده کنید. بهترین بخش این است که برنامه شما به تغییرات بسیار کوچکی برای کد نیاز دارد. Headless تمام کارهای سنگین را انجام می دهد.

در چند خط کد می توانید هر صفحه را 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;
}

چرا از Headless Chrome استفاده کنیم؟

ممکن است به Headless Chrome علاقه مند شوید اگر:

برخی از چارچوب‌ها مانند Preact با ابزارهایی عرضه می‌شوند که رندر سمت سرور را بررسی می‌کنند. اگر فریمورک شما راه حلی برای پیش رندر دارد، به جای اینکه Puppeteer و Headless Chrome را وارد جریان کاری خود کنید، از آن استفاده کنید.

خزیدن در وب مدرن

خزنده‌های موتورهای جستجو، پلتفرم‌های اشتراک‌گذاری اجتماعی، حتی مرورگرها در طول تاریخ منحصراً به نشانه‌گذاری ثابت HTML برای فهرست‌بندی محتوای وب و سطح متکی بوده‌اند. وب مدرن به چیزی بسیار متفاوت تبدیل شده است. برنامه های کاربردی مبتنی بر جاوا اسکریپت در اینجا باقی می مانند، به این معنی که در بسیاری از موارد، محتوای ما می تواند برای ابزارهای خزنده نامرئی باشد.

Googlebot، خزنده جستجوی ما، جاوا اسکریپت را پردازش می‌کند و در عین حال مطمئن می‌شود که تجربه بازدیدکنندگان از سایت را کاهش نمی‌دهد. برخی از تفاوت‌ها و محدودیت‌هایی وجود دارد که باید هنگام طراحی صفحات و برنامه‌های خود در نظر بگیرید تا نحوه دسترسی خزنده‌ها و ارائه محتوای شما را در نظر بگیرید.

صفحات پیش اجرا

همه خزنده ها HTML را درک می کنند. برای اطمینان از اینکه خزنده ها می توانند جاوا اسکریپت را ایندکس کنند، به ابزاری نیاز داریم که:

  • می داند که چگونه انواع جاوا اسکریپت مدرن را اجرا کند و HTML ایستا تولید کند.
  • با افزودن ویژگی های وب به روز می ماند.
  • بدون به روز رسانی کد برنامه شما اجرا می شود.

خوب به نظر می رسد درست است؟ آن ابزار مرورگر است ! Chrome Headless اهمیتی نمی دهد که از چه کتابخانه، چارچوب یا زنجیره ابزاری استفاده می کنید.

به عنوان مثال، اگر برنامه شما با Node.js ساخته شده است، Puppeteer یک راه آسان برای کار با کروم بدون هد است.

با یک صفحه پویا شروع کنید که 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() را از قبل بگیرید و کمی آن را تقویت کنید:

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

تغییرات عمده:

  • حافظه پنهان اضافه شد. کش کردن HTML رندر شده بزرگترین پیروزی برای سرعت بخشیدن به زمان پاسخ است. وقتی صفحه دوباره درخواست می‌شود، از اجرای هدلس کروم به‌کلی اجتناب می‌کنید. بعداً در مورد بهینه سازی های دیگر صحبت خواهم کرد.
  • اگر زمان بارگذاری صفحه تمام شد، مدیریت خطای اصلی را اضافه کنید.
  • تماسی را به 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

Server-Timing API معیارهای عملکرد سرور (مانند زمان درخواست و پاسخ یا جستجوی پایگاه داده) را به مرورگر منتقل می کند. کد مشتری می تواند از این اطلاعات برای ردیابی عملکرد کلی یک برنامه وب استفاده کند.

یک مورد عالی برای زمان‌بندی سرور این است که گزارش دهید چه مدت طول می‌کشد تا کروم بدون هد یک صفحه را از قبل اجرا کند. برای انجام این کار، فقط هدر 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)"
}

نتایج عملکرد

نتایج زیر اکثر بهینه‌سازی‌های عملکردی را که بعداً مورد بحث قرار گرفت، در خود جای داده است.

در یک برنامه نمونه، کروم بدون هد حدود یک ثانیه طول می کشد تا صفحه را در سرور ارائه کند. هنگامی که صفحه کش شد، شبیه سازی DevTools 3G Slow FCP را در 8.37 ثانیه سریعتر از نسخه سمت سرویس گیرنده قرار می دهد.

اولین رنگ (FP) اولین رنگ محتوایی (FCP)
برنامه سمت مشتری 4s 11 ثانیه
نسخه SSR 2.3 ثانیه ~ 2.3 ثانیه

این نتایج امیدوارکننده است. کاربران محتوای معنی‌دار را بسیار سریع‌تر می‌بینند، زیرا صفحه ارائه‌شده در سمت سرور دیگر برای بارگیری + نمایش پست‌ها به جاوا اسکریپت متکی نیست .

جلوگیری از هیدراتاسیون مجدد

یادتان هست که گفتم «ما هیچ تغییری در کد در برنامه سمت مشتری ایجاد نکردیم»؟ این یک دروغ بود.

برنامه Express ما درخواستی را دریافت می کند، از Puppeteer برای بارگذاری صفحه در headless استفاده می کند و نتیجه را به عنوان پاسخ ارائه می دهد. اما این تنظیمات مشکل دارد.

همان جاوا اسکریپتی که در کروم بدون هد روی سرور اجرا می‌شود ، زمانی که مرورگر کاربر صفحه را در قسمت جلویی بارگیری می‌کند، دوباره اجرا می‌شود . ما دو مکان داریم که نشانه گذاری تولید می کنند. #دوبرنده !

برای رفع این مشکل، به صفحه بگویید HTML آن از قبل در جای خود قرار دارد. یک راه حل این است که صفحه جاوا اسکریپت بررسی کند که آیا <ul id="posts"> در زمان بارگذاری در DOM است یا خیر. اگر اینطور است، می‌دانید که صفحه دارای SSR است و می‌توانید از اضافه کردن مجدد پست‌ها اجتناب کنید. 👍

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 حیاتی در صفحه در زمان ساخت، معمول است. این می تواند اولین رنگ معنی دار را سرعت بخشد زیرا مرورگر درخواست های کمتری را در طول بارگذاری صفحه اولیه ارسال می کند.

به جای یک ابزار ساخت جداگانه، از مرورگر به عنوان ابزار ساخت خود استفاده کنید ! ما می‌توانیم از Puppeteer برای دستکاری DOM صفحه، سبک‌های inlining، جاوا اسکریپت، یا هر چیز دیگری که می‌خواهید در صفحه قبل از اجرای اولیه آن بچسبانید، استفاده کنیم.

این مثال نشان می دهد که چگونه می توان پاسخ ها را برای شیوه نامه های محلی رهگیری کرد و آن منابع را به عنوان تگ <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() و ارسال URL اشکال‌زدایی از راه دور نمونه، دوباره به یک نمونه موجود از Chrome متصل شود. برای حفظ یک نمونه مرورگر طولانی مدت، می‌توانیم کدی را که Chrome را راه‌اندازی می‌کند از تابع ssr() به سرور 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};
}

مثال: cron job به صورت دوره ای پیش اجرا

برای ارائه همزمان تعدادی از صفحات، می توانید از یک نمونه مرورگر مشترک استفاده کنید.

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!');
});

همچنین، یک صادرات clearCache() به ssr.js اضافه کنید:

...
function clearCache() {
  RENDER_CACHE.clear();
}

export {ssr, clearCache};

ملاحظات دیگر

یک سیگنال برای صفحه ایجاد کنید: "شما به صورت هدلس رندر می شوید"

هنگامی که صفحه شما توسط کروم بدون هد روی سرور رندر می‌شود، دانستن این موضوع برای منطق سمت سرویس گیرنده صفحه ممکن است مفید باشد. در برنامه‌ام، از این قلاب برای «خاموش کردن» بخش‌هایی از صفحه‌ام استفاده کردم که نقشی در نشان‌گذاری پست‌ها ندارند. به عنوان مثال، من کدی را غیرفعال کردم که firebase-auth.js را با تنبلی بارگذاری می کند. هیچ کاربری برای ورود به سیستم وجود ندارد!

افزودن یک پارامتر ?headless به URL رندر یک راه ساده برای ایجاد یک قلاب صفحه است:

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 در سایت خود استفاده می کنید مراقب باشید. انجام پیش‌اجرای صفحات ممکن است منجر به بازدیدهای زیاد از صفحه شود. به طور خاص، 2 برابر تعداد بازدیدها را مشاهده خواهید کرد ، یک ضربه زمانی که کروم بدون هد صفحه را رندر می‌کند و دیگری زمانی که مرورگر کاربر آن را رندر می‌کند.

پس راه حل چیست؟ از رهگیری شبکه برای لغو هر درخواستی که تلاش می کند کتابخانه 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 با اجرای هدلس کروم، به عنوان یک همراه، در سرور وب شما، رندر صفحات سمت سرور را آسان می کند. "ویژگی" مورد علاقه من در این روش این است که شما عملکرد بارگذاری و نمایه سازی برنامه خود را بدون تغییرات قابل توجه کد بهبود می بخشید!

اگر کنجکاو هستید که یک برنامه کاربردی را ببینید که از تکنیک های شرح داده شده در اینجا استفاده می کند، برنامه devwebfeed را بررسی کنید.

ضمیمه

بحث در مورد هنر قبلی

ارائه برنامه های سمت سرور در سمت سرور سخت است. چقدر سخته فقط نگاه کنید که مردم چند بسته npm نوشته اند که به موضوع اختصاص داده شده است. الگوها ، ابزارها و خدمات بی‌شماری برای کمک به برنامه‌های SSRing JS وجود دارد.

جاوا اسکریپت ایزومورفیک / جهانی

مفهوم جاوا اسکریپت جهانی به این معنی است که: همان کدی که روی سرور اجرا می شود، روی کلاینت (مرورگر) نیز اجرا می شود. شما کد را بین سرور و کلاینت به اشتراک می گذارید و همه یک لحظه ذن را احساس می کنند.

هدلس کروم «JS ایزومورفیک» را بین سرور و کلاینت فعال می‌کند. اگر کتابخانه شما روی سرور (Node) کار نمی کند، یک گزینه عالی است.

ابزارهای پیش اجرا

انجمن Node ابزارهای زیادی برای مقابله با برنامه های SSR JS ساخته است. هیچ شگفتی وجود ندارد! من شخصاً YMMV را با برخی از این ابزارها پیدا کرده‌ام، بنابراین حتماً قبل از انجام یکی از آنها تکالیف خود را انجام دهید. به عنوان مثال، برخی از ابزارهای SSR قدیمی‌تر هستند و از کروم بدون سر (یا هر مرورگر بدون هد) استفاده نمی‌کنند. در عوض، آنها از PhantomJS (معروف به سافاری قدیمی) استفاده می کنند، به این معنی که اگر صفحات شما از ویژگی های جدیدتر استفاده کنند، به درستی رندر نمی شوند.

یکی از استثناهای قابل توجه Prerender است. Prerender از این جهت جالب است که از کروم بدون هد استفاده می‌کند و با میان‌افزار اکسپرس ارائه می‌شود:

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

شایان ذکر است که Prerender جزئیات دانلود و نصب کروم در پلتفرم های مختلف را کنار گذاشته است. اغلب اوقات، درست کردن کار بسیار دشوار است ، که یکی از دلایلی است که Puppeteer برای شما انجام می دهد .