Gözetimsiz Chrome: Sunucu tarafı oluşturma JavaScript siteleri için bir yanıt

Addy Osmani
Addy Osmani

Express web sunucusuna sunucu tarafı oluşturma (SSR) özellikleri eklemek için Puppeteer API'lerini nasıl kullanabileceğinizi öğrenin. En iyi tarafı, uygulamanızda kodda çok küçük değişiklikler yapılması gerekmesidir. Tüm ağır işleri headless yapar.

Birkaç satır kodla herhangi bir sayfayı SSR yapabilir ve nihai işaretlemesini alabilirsiniz.

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

Gözetimsiz Chrome'u neden kullanmalısınız?

Aşağıdaki durumlarda Gözetimsiz Chrome'u kullanabilirsiniz:

Preact gibi bazı çerçeveler, sunucu tarafı oluşturmayı ele alan araçlarla birlikte gönderilir. Çerçevenizde ön oluşturma çözümü varsa Puppeteer ve Headless Chrome'u iş akışınıza dahil etmek yerine bu çözümü kullanın.

Modern web'i tarama

Arama motoru tarayıcıları, sosyal paylaşım platformları ve hatta tarayıcılar, web'i dizine eklemek ve içeriği göstermek için geçmişte yalnızca statik HTML işaretlemesinden yararlanıyordu. Modern web çok farklı bir hâle geldi. JavaScript tabanlı uygulamalar artık kalıcı. Bu da içeriklerimizin birçok durumda tarama araçları tarafından görünemeyeceği anlamına geliyor.

Arama tarayıcımız Googlebot, siteyi ziyaret eden kullanıcıların deneyimini bozmadan JavaScript'i işler. Tarayıcıların içeriğinize nasıl eriştiği ve bunları nasıl oluşturduğuyla alakalı olarak sayfa ve uygulama tasarımında dikkate almanız gereken bazı farklılıklar ve sınırlamalar vardır.

Sayfaları önceden oluşturma

Tüm tarayıcılar HTML'yi anlar. Tarayıcıların JavaScript'i dizine ekleyebilmesi için aşağıdakileri yapan bir araca ihtiyacımız vardır:

  • Modern JavaScript türlerinin tümünü nasıl çalıştıracağını ve statik HTML nasıl oluşturacağını bilir.
  • Web'e yeni özellikler eklendikçe güncel kalır.
  • Uygulamanızda çok az kod güncellemesi veya hiç kod güncellemesi olmadan çalışır.

Kulağa hoş geliyor, değil mi? Bu araç tarayıcıdır. Gözetimsiz Chrome, hangi kitaplığı, çerçeveyi veya araç zincirini kullandığınızı dikkate almaz.

Örneğin, uygulamanız Node.js ile oluşturulduysa Puppeteer, gözetimsiz Chrome ile çalışmanın kolay bir yoludur.

HTML'sini JavaScript ile oluşturan dinamik bir sayfayla başlayın:

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 işlevi

Ardından, daha önce kullandığınız ssr() işlevini biraz daha geliştirin:

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

Önemli değişiklikler:

  • Önbelleğe alma özelliği eklendi. Oluşturulan HTML'yi önbelleğe almak, yanıt sürelerini hızlandırmanın en büyük avantajıdır. Sayfa yeniden istendiğinde, gözetimsiz Chrome'u tamamen çalıştırmaktan kaçınabilirsiniz. Diğer optimizasyonlardan daha sonra bahsedeceğim.
  • Sayfanın yüklenmesi zaman aşımına uğrarsa temel hata işleme ekleyin.
  • page.waitForSelector('#posts') için bir arama ekleyin. Bu sayede, serileştirilmiş sayfayı dökmeden önce gönderilerin DOM'da bulunduğundan emin oluruz.
  • Bilimi ekleyin. Gözetimli olmayanın sayfayı oluşturmasının ne kadar sürdüğünü kaydedin ve oluşturma süresini HTML ile birlikte döndürün.
  • Kodu ssr.mjs adlı bir modüle yapıştırın.

Örnek web sunucusu

Son olarak, tüm bunları bir araya getiren küçük Express sunucusunu görebilirsiniz. Ana işleyici, http://localhost/index.html URL'sini (ana sayfa) önceden oluşturur ve sonucu yanıt olarak sunar. Statik işaretleme artık yanıtın bir parçası olduğundan kullanıcılar sayfaya ulaştığında gönderileri hemen görür.

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

Bu örneği çalıştırmak için bağımlılıkları (npm i --save puppeteer express) yükleyin ve --experimental-modules işaretçisini kullanarak sunucuda Node 8.5.0 veya sonraki bir sürümü çalıştırın:

Bu sunucu tarafından geri gönderilen yanıtın bir örneğini aşağıda bulabilirsiniz:

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

Yeni Server-Timing API için mükemmel bir kullanım alanı

Server-Timing API'si, sunucu performansı metriklerini (ör. istek ve yanıt süreleri veya veritabanı aramaları) tarayıcıya iletir. İstemci kodu, bir web uygulamasının genel performansını izlemek için bu bilgileri kullanabilir.

Server-Timing için mükemmel bir kullanım alanı, gözetimsiz Chrome'un bir sayfayı önceden oluşturmasının ne kadar sürdüğünü bildirmektir. Bunu yapmak için Server-Timing üstbilgisini sunucu yanıtına eklemeniz yeterlidir:

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

İstemcide, aşağıdaki metriklere erişmek için Performance API ve PerformanceObserver kullanılabilir:

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

Performans sonuçları

Aşağıdaki sonuçlar, daha sonra açıklanan performans optimizasyonlarının çoğunu içerir.

Örnek bir uygulamada, gözetimsiz Chromium'un sayfayı sunucuda oluşturması yaklaşık bir saniye sürer. Sayfa önbelleğe alındıktan sonra DevTools 3G Yavaş emülasyonu, FCP'yi istemci tarafı sürümden 8,37 saniye daha hızlı hale getirir.

İlk Boyama (FP)First Contentful Paint (FCP)
İstemci tarafı uygulama4 sn. 11 sn.
SSR sürümü2,3 sn.~2,3 sn.

Bu sonuçlar umut verici. Sunucu tarafında oluşturulan sayfa artık yükleme ve gönderileri göstermek için JavaScript'e ihtiyaç duymadığından kullanıcılar anlamlı içerikleri çok daha hızlı görür.

Yeniden sulanmayı önleme

"İstemci tarafı uygulamada herhangi bir kod değişikliği yapmadık" dediğimi hatırlıyor musunuz? Bu bir yalandı.

Express uygulamamız bir istek alır, sayfayı headless'a yüklemek için Puppeteer'ı kullanır ve sonucu yanıt olarak sunar. Ancak bu kurulumda bir sorun var.

Kullanıcının tarayıcısı sayfayı ön uçta yüklediğinde, sunucuda gözetimsiz Chrome'da yürütülen aynı JavaScript tekrar çalışır. İşaretleme oluşturmanın iki yolu vardır. #doublerender

Bu sorunu düzeltmek için sayfaya HTML'sinin zaten mevcut olduğunu bildirin. Bir çözüm, sayfa JavaScript'inin yükleme sırasında <ul id="posts"> öğesinin DOM'da olup olmadığını kontrol etmesini sağlamaktır. Bu durumda, sayfanın SSR'ye tabi tutulduğunu bilir ve yayınları tekrar eklemekten kaçınabilirsiniz. 👍

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>

Optimizasyonlar

Oluşturulan sonuçları önbelleğe almanın yanı sıra ssr() için yapabileceğimiz birçok ilginç optimizasyon vardır. Bazıları hızlı kazanç sağlarken bazıları daha spekülatif olabilir. Gördüğünüz performans avantajları, önceden oluşturacağınız sayfa türlerine ve uygulamanın karmaşıklığına bağlı olabilir.

Önemli olmayan istekleri iptal etme

Şu anda sayfanın tamamı (ve istenen tüm kaynaklar) koşulsuz olarak başlıksız Chrome'a yüklenir. Ancak yalnızca iki konuyla ilgileniyoruz:

  1. Oluşturulan işaretleme.
  2. Bu işaretlemeyi oluşturan JS istekleri.

DOM oluşturmayan ağ istekleri israftır. Resimler, yazı tipleri, stil sayfaları ve medya gibi kaynaklar bir sayfanın HTML'sinin oluşturulmasına dahil edilmez. Sayfaların stilini belirler ve yapısını destekler ancak sayfayı açıkça oluşturmazlar. Tarayıcıya bu kaynakları yoksaymasını söylemeliyiz. Bu sayede, gözetimsiz Chrome'un iş yükü azalır, bant genişliğinden tasarruf edilir ve büyük sayfaların ön oluşturma süresi kısaltılabilir.

DevTools Protokolü, ağ müdahalesi adlı güçlü bir özelliği destekler. Bu özellik, isteklerin tarayıcı tarafından gönderilmeden önce değiştirilmesi için kullanılabilir. Puppeteer, page.setRequestInterception(true)'u etkinleştirip sayfanın request etkinliğini dinleyerek ağ müdahalesini destekler. Bu sayede belirli kaynaklara yönelik istekleri iptal edebilir ve diğer isteklerin devam etmesine izin verebiliriz.

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

Satır içi kritik kaynaklar

Bir uygulamayı işlemek ve derleme sırasında kritik CSS ve JS'yi sayfaya satır içi yapmak için ayrı derleme araçları (gulp gibi) kullanmak yaygındır. Bu, tarayıcı ilk sayfa yükleme sırasında daha az istek gönderdiği için ilk anlamlı boyamayı hızlandırabilir.

Ayrı bir derleme aracı yerine derleme aracı olarak tarayıcıyı kullanın. Sayfayı önceden oluşturmadan önce sayfanın DOM'unu, satır içi stilleri, JavaScript'i veya sayfaya eklemek istediğiniz diğer öğeleri değiştirmek için Puppeteer'i kullanabiliriz.

Bu örnekte, yerel stil sayfalarına yönelik yanıtların nasıl yakalanacağı ve bu kaynakların <style> etiketleri olarak sayfaya nasıl yerleştirileceği gösterilmektedir:

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

Tek bir Chrome örneğini oluşturma işlemlerinde yeniden kullanma

Her ön oluşturma için yeni bir tarayıcı başlatmak çok fazla ek yük oluşturur. Bunun yerine, tek bir örnek başlatıp birden fazla sayfayı oluşturmak için yeniden kullanabilirsiniz.

Puppeteer, puppeteer.connect() işlevini çağırıp örneğin uzak hata ayıklama URL'sini ileterek mevcut bir Chrome örneğine yeniden bağlanabilir. Uzun süre çalışan bir tarayıcı örneğini korumak için Chrome'u başlatan kodu ssr() işlevinden Express sunucusuna taşıyabiliriz:

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

Örnek: Periyodik olarak ön oluşturma yapmak için cron işi

Birden fazla sayfayı aynı anda oluşturmak için paylaşılan bir tarayıcı örneği kullanabilirsiniz.

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

Ayrıca ssr.js dosyasına bir clearCache() dışa aktarma işlevi ekleyin:

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

export {ssr, clearCache};

Dikkat edilmesi gereken diğer noktalar

Sayfa için bir sinyal oluşturun: "Başsız olarak oluşturuluyorsunuz"

Sayfanız sunucudaki headless Chrome tarafından oluşturulurken sayfanın istemci tarafı mantığının bunu bilmesi yararlı olabilir. Uygulamamda, sayfamdaki yayın işaretlemesinin oluşturulmasında rol oynamayan bölümleri "devre dışı bırakmak" için bu kancayı kullandım. Örneğin, firebase-auth.js dosyasını gecikmeli olarak yükleyen kodu devre dışı bıraktım. Oturum açacak kullanıcı yok.

Oluşturma URL'sine bir ?headless parametresi eklemek, sayfaya kanca eklemenin basit bir yoludur:

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

Sayfada bu parametreyi arayabiliriz:

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 sayfa görüntüleme sayısının şişirilmesini önleme

Sitenizde Analytics kullanıyorsanız dikkatli olun. Sayfaların önceden oluşturulması, sayfa görüntüleme sayısının şişmesine neden olabilir. Daha açık belirtmek gerekirse, 2 kat daha fazla isabet görürsünüz. Bunlardan biri, başlıksız Chrome sayfayı oluştururken, diğeri ise kullanıcının tarayıcısı sayfayı oluştururken gerçekleşir.

Peki çözüm nedir? Analytics kitaplığını yüklemeye çalışan tüm istekleri iptal etmek için ağ müdahalesini kullanın.

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

Kod hiç yüklenmezse sayfa isabetleri hiçbir zaman kaydedilmez. Boom 💥.

Alternatif olarak, sunucunuzun kaç ön oluşturma işlemi gerçekleştirdiği hakkında bilgi edinmek için Analytics kitaplıklarınızı yüklemeye devam edin.

Sonuç

Puppeteer, web sunucunuzda gözetimsiz Chrome'u yardımcı olarak çalıştırarak sayfaları sunucu tarafında oluşturmayı kolaylaştırır. Bu yaklaşımın en sevdiğim "özelliği", önemli kod değişiklikleri yapmadan uygulamanızın yükleme performansını ve dizine eklenebilirliğini iyileştirmenizdir.

Burada açıklanan teknikleri kullanan çalışan bir uygulama görmek istiyorsanız devwebfeed uygulamasına göz atın.

Ek

Önceki çalışmaların tartışılması

İstemci tarafı uygulamaları sunucu tarafında oluşturmak zordur. Ne kadar zor? Kullanıcıların konuyla ilgili yazdığı npm paketlerinin sayısına bakmanız yeterlidir. JS uygulamalarını SSR'ye dönüştürme konusunda yardımcı olacak sayısız model, araç ve hizmet vardır.

Isomorphic / Universal JavaScript

Universal JavaScript kavramı, sunucu üzerinde çalışan kodun istemcide (tarayıcıda) de çalışacağı anlamına gelir. Kodu sunucu ile istemci arasında paylaşırsınız ve herkes rahatlar.

Gözetimli olmayan Chrome, sunucu ile istemci arasında "izomorfik JS" sağlar. Kitaplığınız sunucuda (Node) çalışmıyorsa bu seçenek idealdir.

Önceden oluşturma araçları

Node topluluğu, SSR JS uygulamalarıyla ilgili birçok araç geliştirmiştir. Bu, beklediğimiz bir sonuçtu. Kişisel olarak, bu araçlardan bazılarının YMMV olduğunu fark ettim. Bu nedenle, bir araç kullanmaya karar vermeden önce mutlaka araştırmanızı yapın. Örneğin, bazı SSR araçları eskidir ve gözetimsiz Chrome'u (veya bu konuda herhangi bir gözetimsiz tarayıcı) kullanmaz. Bunun yerine PhantomJS'yi (eski Safari) kullanırlar. Bu, sayfalarınızda daha yeni özellikler kullanılıyorsa sayfaların düzgün şekilde oluşturulmayacağı anlamına gelir.

Önceden oluşturma, dikkate değer istisnalardan biridir. Prerender, gözetimsiz Chrome kullanması ve Express için hazır ara yazılım ile birlikte gelmesi açısından ilgi çekicidir:

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

Önceden oluşturma özelliğinin, Chrome'u farklı platformlara indirme ve yükleme ayrıntılarını içermediğini unutmayın. Bunu doğru şekilde yapmak genellikle oldukça zordur. Puppeteer'ın sizin için bunu yapmasının nedenlerinden biri budur.