Node.js SDK
ใช้ @growthbook/growthbook package เดียวกันที่รันใน browser ได้เลยใน Node.js ไม่ต้องติดตั้ง server SDK แยกต่างหาก ความแตกต่างหลักอยู่ที่ lifecycle model: บน server การประเมิน flag เกิดขึ้นภายใน request handlers แทนที่จะเป็นครั้งเดียวตอน app startup
การประเมิน flag ฝั่ง Server
หัวข้อที่มีชื่อว่า “การประเมิน flag ฝั่ง Server”ใน browser app มักสร้าง GrowthBook instance เดียวตลอดอายุของ page แต่บน server แต่ละ HTTP request อาจมีผู้ใช้ต่างกันโดยสิ้นเชิง จึงต้องใช้ per-request scoping — สร้าง (หรือ clone) instance ใหม่ต่อทุก request และ set user-specific attributes บน instance นั้น
ความแตกต่างจาก Browser
หัวข้อที่มีชื่อว่า “ความแตกต่างจาก Browser”บน server แต่ละ HTTP request อาจมีผู้ใช้ต่างกัน จึงต้องสร้าง GrowthBook instance (หรือ clone) ต่อ request และ set user-specific attributes บน instance นั้น แทนที่จะแชร์ instance เดียวตลอดอายุของ process
Cache Payload ระหว่าง Requests
หัวข้อที่มีชื่อว่า “Cache Payload ระหว่าง Requests”การดึง feature definitions จาก GrowthBook ทุก request จะเพิ่ม latency และสิ้นเปลือง bandwidth แทนที่ด้วยการดึง payload ครั้งเดียวตอน startup เก็บไว้ใน memory แล้ว reuse: gb.init({ payload: cachedPayload }) พร้อม cached payload วิธีนี้ข้ามการ network call ไปได้เลยโดยยังคง flag evaluation ที่ถูกต้อง
ดึง payload จาก loader instance ด้วย gb.getPayload() แล้วส่งให้ instance ใหม่ผ่าน gb.setPayload(payload) หรือส่งตรงให้ init
ทำไมต้อง Scope ต่อ Request
หัวข้อที่มีชื่อว่า “ทำไมต้อง Scope ต่อ Request”ถ้าแชร์ GrowthBook instance เดียวกันที่มี mutable attributes ข้าม requests และเรียก gb.setAttributes(...) ในแต่ละ handler, concurrent requests จะเขียนทับ user context ของกัน — ทำให้การประเมิน flag ผิดพลาดและหาบั๊กได้ยาก การสร้าง instance ใหม่ต่อ request กำจัด race condition นี้ได้อย่างสมบูรณ์
import { GrowthBook, setPolyfills } from '@growthbook/growthbook';
// --- App startup: fetch and cache the payload once ---
let cachedPayload = null;
const loader = new GrowthBook({
apiHost: 'https://cdn.growthbook.io',
clientKey: 'sdk-abc123',
});
await loader.init({ timeout: 3000 });
cachedPayload = loader.getPayload();
// --- Per-request handler ---
async function handleRequest(req, res) {
// Fresh instance per request — no shared mutable state
const gb = new GrowthBook({
apiHost: 'https://cdn.growthbook.io',
clientKey: 'sdk-abc123',
});
// Reuse the cached payload — no extra network call
await gb.init({ payload: cachedPayload });
// Scope attributes to this request's user
gb.setAttributes({
id: req.user.id,
country: req.headers['cf-ipcountry'] ?? 'US',
});
const useBetaUI = gb.isOn('beta-ui');
res.json({ useBetaUI });
gb.destroy(); // free listeners
}ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| Server-side SDK | ปลอดภัยกว่า ควบคุม logic backend ได้เต็มที่ | ต้องจัดการ caching/refresh เองเพื่อไม่ query ทุก request |
| เรียก API ตรงทุก request | ง่ายต่อการเข้าใจ | latency สูงและ load เพิ่มต่อ backend service ของ GrowthBook |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- re-fetch feature payload ใหม่ทุก request แทนที่จะดึงครั้งเดียวแล้ว reuse — การสร้าง instance ใหม่ต่อ request นั้นเบาและเป็นแนวทางที่แนะนำ ตัวที่สิ้นเปลืองคือ network fetch ของ payload ต่างหาก
- ไม่ cache feature payload ทำให้ทุก request ยิง network call ไปที่ CDN
- ลืม refresh payload เมื่อ flag เปลี่ยนใน UI
💡 ตัวอย่างจากของจริง
backend service ขนาดใหญ่มัก fetch feature payload ครั้งเดียวต่อ process แล้ว background refresh เก็บไว้ใน memory จากนั้นสร้าง instance ใหม่แบบเบา ๆ ต่อ request โดย reuse payload ที่ cache ไว้ — ได้ทั้ง targeting ที่แยกต่อ request และ latency ที่ใกล้ศูนย์เพราะไม่ต้อง re-fetch