Express web sunucusuna sunucu tarafı oluşturma (SSR) özellikleri eklemek için Puppeteer API'lerini nasıl kullanabileceğinizi öğrenin. En iyi yanı, uygulamanızın kodda çok küçük değişiklikler gerektirmesidir. İşin zor kısmını gözetimsiz hale getirir.
Birkaç satır kod girerek herhangi bir sayfayı SSR ve son 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 ilginizi çekebilir:
- Arama motorları tarafından dizine eklenmeyen bir web uygulaması oluşturdunuz.
- JavaScript performansını optimize etmek ve ilk anlamlı boyamayı iyileştirmek için hızlı bir kazanç elde etmeyi umuyorsunuz.
Preact araçlarla gönderim gibi bazı çerçeveler: Çerçevenizde bir önceden işleme çözümü varsa Puppeteer ve Headless Chrome'u iş akışınıza dahil etmek yerine bu çözüme bağlı kalın.
Modern web'de tarama
Arama motoru tarayıcıları, sosyal paylaşım platformları ve hatta tarayıcılar, geçmişte web'i dizine eklemek ve içerik sunmak için yalnızca statik HTML işaretlemesini kullanıyordu. Modern web çok farklı bir şeye dönüştü. JavaScript tabanlı uygulamalar kullanımda kalacak. Bu, çoğu durumda içeriğimizin tarama araçları tarafından görülmeyebileceği anlamına geliyor.
Arama tarayıcımız olan Googlebot, siteyi ziyaret eden kullanıcıların deneyimini bozmadığından emin olarak JavaScript'i işler. Sayfalarınızı ve uygulamalarınızı tarayıcıların içeriğinize nasıl eriştiğine ve bunları nasıl oluşturduğuna göre tasarlarken dikkat etmeniz gereken bazı farklar ve sınırlamalar vardır.
Sayfaları önceden oluştur
Tüm tarayıcılar HTML'yi anlar. Tarayıcıların JavaScript'i dizine ekleyebilmesi için şunu yapan bir araca ihtiyacımız vardır:
- Tüm modern JavaScript türlerini nasıl çalıştıracağını ve statik HTML oluşturmayı bilir.
- Web yeni özellikler ekledikçe güncel kalır.
- Uygulamanızda çok az kod güncellemesiyle veya güncelleme olmadan çalışır.
Kulağa hoş geliyor, değil mi? Bu araç tarayıcıdır! Gözetimsiz Chrome, kullandığınız kitaplığı, çerçeveyi veya araç zincirini umursamaz.
Örneğin, uygulamanız Node.js ile derlenmişse Puppeteer, gözetimsiz Chrome ile çalışmanın kolay bir yoludur.
HTML'sini JavaScript'le 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
Daha sonra, önceki ssr()
işlevini alıp biraz artırın:
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 eklendi. Yanıt sürelerini hızlandırmak için oluşturulan HTML'yi önbelleğe almak en büyük kazançtır. Sayfa yeniden istendiğinde, gözetimsiz Chrome'u birlikte çalıştırmaktan kaçınmış olursunuz. Diğer optimizasyonlardan ileride bahsedeceğim.
- Sayfanın yüklenmesi zaman aşımına uğruyorsa temel hata işleme yöntemini ekleyin.
page.waitForSelector('#posts')
adlı kişiye arama ekleyin. Bu, serileştirilmiş sayfanın dökümünden önce yayınların DOM'da var olmasını sağlar.- Bilim ekle'yi tıklayın. Gözetimsiz sayfanın ne kadar sürede oluşturulduğunu ve oluşturma süresini HTML ile birlikte döndürmenin ne kadar sürdüğünü kaydedin.
- Kodu
ssr.mjs
adlı bir modüle yapıştırın.
Örnek web sunucusu
Son olarak, işte bunların hepsini bir araya getiren küçük express sunucusu. 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ıklarında yayınları 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 Düğüm 8.5.0+ ve --experimental-modules
işaretini kullanarak sunucuyu çalıştırın:
Bu sunucu tarafından gönderilen yanıtın bir örneğini burada görebilirsiniz:
<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, sunucu performansı metriklerini (istek ve yanıt süreleri veya veritabanı aramaları gibi) tarayıcıya geri iletir. İstemci kodu, bu bilgileri bir web uygulamasının genel performansını izlemek için kullanabilir.
Sunucu Zamanlaması için mükemmel bir kullanım örneği, gözetimsiz Chrome'un bir sayfayı önceden oluşturmasının ne kadar sürdüğünü bildirmektir. Bunu yapmak için sunucu yanıtına Server-Timing
üst bilgisini 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 tartışılan performans optimizasyonlarının çoğunu içerir.
Örnek bir uygulamada, gözetimsiz Chrome'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 taraflı sürüme göre 8,37 sn daha hızlı yerleştirir.
İlk Boyama (FP) | First Contentful Paint (FCP) | |
---|---|---|
İstemci tarafı uygulama | 4 sn. | 11 sn. |
SSR sürümü | 2,3 sn. | ~2,3 sn. |
Bu sonuçlar umut verici. Sunucu tarafında oluşturulan sayfa yayınları yüklemek ve göstermek için artık JavaScript'e ihtiyaç duymadığından kullanıcılar anlamlı içerikleri çok daha hızlı görür.
Sıvı kaybını önleyin
"İstemci taraflı uygulamada herhangi bir kod değişikliği yapmadık" dediğimi hatırlıyor musunuz? Bu yalan.
Express uygulamamız isteği alır, sayfayı gözetimsiz olarak yüklemek için Puppeteer'ı kullanır ve sonucu yanıtlar. 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 çalışan aynı JavaScript tekrar çalışır. İşaretlemeyi oluşturan iki yerimiz vardır. #doublerender!
Bunu düzeltmek için sayfaya HTML'sinin zaten ayarlanmış olduğunu bildirin.
Uygulayabileceğiniz çözümlerden biri, sayfa JavaScript'inin, yükleme sırasında <ul id="posts">
öğesinin zaten DOM'de olup olmadığını kontrol etmesini sağlamaktır. Öyleyse, sayfaya SSR'nin uygulandığını bilirsiniz ve yayınları yeniden 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 dışında, ssr()
için yapabileceğimiz birçok ilginç optimizasyon var. Bazıları hızlı kazanımlar elde ederken bazıları
daha spekülatif durabilir. Gördüğünüz performans avantajları sonuçta, önceden oluşturduğunuz sayfa türlerine ve uygulamanın karmaşıklığına bağlı olabilir.
Zorunlu olmayan istekleri iptal et
Şu anda sayfanın tamamı (ve istekte bulunduğu tüm kaynaklar) gözetimsiz Chrome'a koşulsuz olarak yükleniyor. Ancak, ilgilendiğimiz iki nokta:
- Oluşturulan işaretleme.
- Bu işaretlemeyi oluşturan JS istekleri.
DOM oluşturmayan ağ istekleri boşadır. Resimler, yazı tipleri, stil sayfaları ve medya gibi kaynaklar, bir sayfanın HTML'sini oluşturmak için kullanılmaz. Bunlar bir sayfanın stilini oluşturur ve yapısını tamamlar, ancak sayfayı açık bir şekilde oluşturmaz. Tarayıcıya bu kaynakları göz ardı etmesini bildirmelisiniz. Bu da gözetimsiz Chrome'un iş yükünü azaltır, bant genişliğini azaltır ve daha büyük sayfaların önceden oluşturma süresini potansiyel olarak hızlandırır.
DevTools Protokolü, Ağ müdahalesi adı verilen güçlü bir özelliği destekler. Bu özellik, istekleri tarayıcı tarafından yayınlanmadan önce değiştirmek için kullanılabilir.
Puppeteer, page.setRequestInterception(true)
özelliğini açıp sayfanın request
etkinliğini dinleyerek ağ müdahalesini destekler.
Bu sayede belirli kaynaklara yönelik istekleri iptal edip diğerlerini kullanmaya devam edebiliriz.
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};
}
Kritik kaynakları satır içine alın
Bir uygulamayı işlemek ve derleme sırasında önemli CSS ile JS'leri sayfada satır içi yapmak için ayrı derleme araçları (gulp
gibi) yaygın olarak kullanılır. Tarayıcı ilk sayfa yükleme sırasında daha az istek yaptığından, bu işlem ilk anlamlı boyamayı hızlandırabilir.
Ayrı bir derleme aracı yerine, yapı aracınız olarak tarayıcıyı kullanın! Sayfanın DOM'sini, satır içi stillerini, JavaScript'ini veya önceden oluşturmadan önce sayfada kalmasını istediğiniz diğer öğeleri değiştirmek için Puppeteer'ı kullanabiliriz.
Bu örnekte, yerel stil sayfaları için yanıtlara nasıl müdahale edileceği ve bu kaynakların sayfada <style>
etiketleri olarak satır içine nasıl ekleneceğ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:
- 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};
}
Oluşturma işlemlerinde tek bir Chrome örneğini yeniden kullanma
Her önceden işleme için yeni bir tarayıcının başlatılması önemli miktarda ek yük oluşturur. Bunun yerine, tek bir örnek başlatmak ve birden çok sayfa oluşturmak için onu yeniden kullanmak isteyebilirsiniz.
Puppeteer, puppeteer.connect()
çağrısı yaparak ve örneğin uzaktan 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: düzenli olarak önceden oluşturmak için cron işi
Birkaç sayfanın tümünü 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 aktarımı ekleyin:
...
function clearCache() {
RENDER_CACHE.clear();
}
export {ssr, clearCache};
Dikkat edilmesi gereken diğer noktalar
Sayfa için sinyal oluşturun: "Gözetimsiz olarak oluşturuluyor".
Sayfanız sunucuda gözetimsiz Chrome tarafından oluşturulurken, sayfanın istemci tarafı mantığının bunu bilmesi faydalı olabilir. Uygulamamda, sayfamın yayın işaretlemesinin oluşturulmasında rol oynamayan bölümlerini "kapatmak" için bu kancayı kullandım. Örneğin, firebase-auth.js öğesini geç yükleyen kodu devre dışı bıraktım. Oturum açabilecek kullanıcı yok!
Oluşturma URL'sine bir ?headless
parametresi eklemek, sayfaya ilgi çekici 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ülemelerini şişirmeyi önleme
Sitenizde Analytics kullanıyorsanız dikkatli olun. Sayfaların önceden oluşturulması, sayfa görüntülemelerinin şişirilmesine neden olabilir. Özellikle, gözetimsiz Chrome'un sayfayı oluşturduğunda ve kullanıcının tarayıcısı tarafından oluşturulduğunda başka bir isabet olmak üzere 2 kat daha fazla isabet görürsünüz.
Peki çözüm nedir? Analytics kitaplığını yüklemeye çalışan 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çbir zaman yüklenmezse sayfa isabetleri hiçbir zaman kaydedilmez. Boom 🎥.
Alternatif olarak, sunucunuzun kaç önceden işleme performansı gösterdiğiyle ilgili bilgi edinmek için Analytics kitaplıklarınızı yüklemeye devam edin.
Sonuç
Puppeteer, web sunucunuzda gözetimsiz Chrome'u tamamlayıcı olarak çalıştırarak sunucu tarafında sayfa 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ı iyileştirmeniz ve dizine eklenebilmesidir.
Burada açıklanan teknikleri kullanan, çalışan bir uygulama görmek istiyorsanız devwebfeed uygulamasına göz atın.
Ek
Önceki tekniklerin tartışılması
İstemci taraflı uygulamaların sunucu tarafında oluşturulması zordur. Ne kadar zor? Konuya özel kaç npm paketi yazdığına bakın. SSRing JS uygulamalarında yardımcı olabilecek çok sayıda kalıp, tools ve hizmet vardır.
İzomorfik / Evrensel JavaScript
Evrensel JavaScript kavramı şu anlama gelir: Sunucuda çalışan aynı kod, istemcide de (tarayıcı) çalışır. Sunucu ve istemci arasında kod paylaşıyorsunuz ve herkes rahat hissediyor.
Gözetimsiz Chrome, sunucu ile istemci arasında "izomorfik JS"yi etkinleştirir. Bu, kitaplığınız sunucuda (Düğüm) çalışmıyorsa harika bir seçenektir.
Önceden oluşturma araçları
Node topluluğu, SSR JS uygulamalarıyla çalışmak için pek çok araç geliştirmiştir. Sürpriz yok! Şahsen bu araçların bazılarının YMMV olduğunu gördüm; bu yüzden, bir aracı kullanmaya başlamadan önce mutlaka ödevinizi yapın. Örneğin, bazı SSR araçları daha eskidir ve gözetimsiz Chrome'u (veya bu konuda gözetimsiz herhangi bir tarayıcıyı) kullanmaz. Bunun yerine PhantomJS (eski Safari) kullanırlar. Bu da, yeni özellikler kullanan sayfalarınızın düzgün şekilde oluşturulmayacağı anlamına gelir.
Önemli istisnalardan biri Önceden işleme'dir. Önceden oluşturma, gözetimsiz Chrome kullanması ve Express için açılır ara katman yazılımı içermesi 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ı vermediğini belirtmekte fayda var. Çoğu zaman, doğru yapmak oldukça zordur. Bu da Kuklacı'nın sizin yerinize çalışmasının nedenlerinden biridir.