ในบทความนี้เกี่ยวกับ Upstash for Redis และ Performance API เราจะดูวิธีใช้ Upstash for Redis ใน Deno app ได้ดีที่สุด Upstash for Redis คือ serverless database ที่เหมาะสำหรับ server-side caching เว็บแอปที่ผมทำงานอยู่ได้คะแนนแย่ใน Initial Server Response Time Lighthouse รายงาน 500 ms ด้วยการเพิ่ม Upstash cache ลดเหลือ ต่ำกว่า 150 ms และผ่าน audit ส่วนยากไม่ใช่การเพิ่ม cache แต่การหาจุดที่ควรใช้ cache ถึงขั้นสำคัญ เพียงแค่การวัดประสิทธิภาพจึง ระบุ bottleneck ได้ เพิ่มประสิทธิภาพด้วยงานน้อยที่สุด เราจะดู performance measurement ในบทความนี้
โปรเจกต์ของผมเป็น Deno Fresh web app Deno มี instant builds และ deploys ทำให้เป็นสภาพแวดล้อมฝันสำหรับ optimization feedback loop สั้น คุณ code optimization locally push ไป server ทดสอบ remote site ได้ทันที
Stack
ที่นี่จะพูดถึงการปรับปรุงประสิทธิภาพด้วย skeleton app ใช้ tooling ต่อไปนี้:
upstash_redis: Deno module สำหรับทำงานกับ Upstash for Redis- Deno Fresh: framework ใหม่ production-ready สำหรับสร้าง server-side rendered (SSR) apps ใน Deno
- serverless logging: ใช้ console ที่นี่ แต่สำหรับ deployed app เพื่อเข้าถึง live measurements ต้องใช้ service เช่น Logtail
Setup
ความแตกต่างสำคัญระหว่าง Node และ Deno คือวิธีเข้าถึง third-party modules Deno ใช้ URLs และ import maps แทน package.json Full URL สำหรับ upstash_redis เช่น https://deno.land/x/upstash_redis@v1.20.0 เริ่มต้นด้วยการสร้าง Deno Fresh app ใหม่
deno run -A -r https://fresh.deno.dev upstash-redis-deno-perf
ตั้งค่า Deno บนระบบได้ด้วยคำสั่ง Terminal ไม่กี่คำสั่ง ถ้าลองครั้งแรก
ตอนนี้เพิ่ม Upstash ใน import_map.json ใน project root directory:
{
"imports": {
"@/": "./",
"$fresh/": "https://deno.land/x/fresh@1.1.2/",
// ...TRUNCATED
"$std/": "https://deno.land/std@0.177.0/",
"upstash/": "https://deno.land/x/upstash_redis@v1.20.0/"
}
}
@/กำหนด import alias สำหรับความสะดวก ให้ importcomponents(ใน project root directory) ด้วย@/componentsไม่ว่า source file จะอยู่โฟลเดอร์ไหน$std/เป็น alias สำหรับ Deno Standard Library ที่มี utility function สำหรับอ่าน.envenvironment variable filesupstash/ให้เข้าถึง Upstash for Redis library จาก TypeScript หรือ JavaScript source file ใดๆ ในโปรเจกต์
Upstash for Redis และ Performance API: Skeleton App
Skeleton app จะดึงข้อมูล:
- page views ใน 28 วันที่ผ่านมา ใช้ web analytics จาก Tinybird (serverless Clickhouse)
- page likes ใช้ Webmentions
ข้อมูลเหล่านี้มาจาก fetch requests ทำให้ server tasks ค่อนข้าง representative ของ real-world business app ตัวแปร likes และ views เก็บ responses จาก API ทั้งสองซึ่งเราแสดงใน frontend

Server handler code สำหรับหน้านั้นประมาณนี้:
export const handler: Handlers<Data> = {
async GET(request, context) {
const { url } = request;
const { pathname } = new URL(url);
const likes = await getWebmentionLikes(pathname);
const views = await getTinybirdViews({ days: 28 });
return context.render({ likes, views });
},
};
Extract pathname จาก request object ใช้ pathname ใน data helper functions สุดท้าย return ค่าที่ดึงมาจาก remote
เพื่อประสิทธิภาพ เรียก helper functions แบบ restructured:
const [likes, views] = await Promise.all([
getWebmentionLikes(pathname),
getTinybirdViews({ days: 28 }),
]);
JavaScript Performance Web API
Performance measurement ควรเป็นขั้นแรกในการ optimize Rate limiting step ไม่ได้เป็นสิ่งที่คาดการณ์ไว้เสมอไป โดยไม่วัด คุณอาจใช้เวลาและทรัพยากรบนสิ่งที่กลายเป็น suboptimal solution Performance API ช่วยได้มาก ในส่วนนี้เราจะดูวิธีใช้เพื่อกำหนดว่าที่ไหนควรใช้ Upstash for Redis ในแอป
window.performance ให้เข้าถึง Performance Web API จาก client browser Deno รองรับ Web APIs บน server ดังนั้น performance พร้อมใช้งาน globally ใน Deno server-side code นี่คือ 2 performance methods ที่ต้องการใช้:
performance.mark('your-mark-name'): สร้างPerformanceMarkobject แทนจุดในเวลา parameter name ใช้ตอนสร้าง measure กับ markperformance.measure('your description', startMarkName, finishMarkName): สร้างPerformanceMeasureobject ผูก start และ finish time marks กับ label มีประโยชน์สำหรับ logging และคำนวณว่า event ใช้เวลานานแค่ไหน
timeEvent: Performance Helper Function
ตอนนี้รู้พื้นฐานแล้ว สร้างฟังก์ชัน timeEvent รับ array ของ PerformanceMeasure objects และ function ที่ต้องการวัดเวลาเป็น input timeEvent จะสร้าง start mark เรียก function ที่ส่งมา แล้วสร้าง finish mark ทันที สุดท้าย mutate array ของ PerformanceMeasure objects ที่รับมา เพิ่ม measure ใหม่ โค้ดจาก utils/performance.ts:
export async function timeEvent<EventReturnType>(
eventFunction: () => Promise<EventReturnType>,
{ description, performanceMeasures, }: { description: string; performanceMeasures: PerformanceMeasure[] },
): Promise<EventReturnType> {
// prepare
const startName = `${description}-started`;
const finishName = `${description}-finished`;
// time
performance.mark(startName);
const result = await eventFunction();
performance.mark(finishName);
// record
performanceMeasures.push(
performance.measure(description, startName, finishName),
);
return result;
}
ฟังก์ชันนี้เป็น generic แม้สำหรับ skeleton app EventReturnType จะเป็น number เสมอ
อัปเดต Server Code ด้วย Measurements
ใช้ timeEvent function ใหม่ใน handler เริ่มรัน comparison โค้ด server handler ที่อัปเดต:
import type { Handlers, PageProps } from "$fresh/server.ts";
import "$std/dotenv/load.ts";
/* included for visibility here, typically you can import once for project in `dev.ts` */
import { timeEvent } from "@/utils/performance.ts";
// ...TRUNCATED
export const handler: Handlers<Data> = {
async GET(request, context) {
// ...TRUNCATED
const performanceMeasures: PerformanceMeasure[] = [];
const [likes, views] = await Promise.all([
timeEvent<number>(() => getWebmentionLikes(pathname), {
description: "web-mention-likes",
performanceMeasures,
}),
timeEvent<number>(() => getTinybirdViews({ days: 28 }), {
description: "analytics-views",
performanceMeasures,
}),
]);
// Replace with a serverless logging service for production
console.log({ performanceMeasures });
return context.render({ likes, views });
},
};
timeEvent function return ผลลัพธ์ของ function ที่ส่งเข้าไป ทำให้แค่ wrap data helper functions ทั้งสองที่มีก่อนหน้าใน timeEvent calls เรารัน locally ที่นี่ สำหรับ production app ต้องรัน measurements บน live site เนื่องจาก backbone connections บน server จะ perform ต่างจาก local ตอนรันบน server ใช้ logging service เช่น Logtail บันทึก measures

ใน console log capture มองเห็น PerformanceMeasure object ให้ measure values:
- name
- start time
- duration (ใน milliseconds)
ในที่นี้เราต้องการค่าข้อมูลทั้งสอง (likes และ views) เพื่อ render app ทั้งคู่ใช้เวลาประมาณเท่ากัน (2 วินาที) ถ้าฟังก์ชันหนึ่งช้ากว่าอีกอันมาก เราจะเพิ่ม Upstash for Redis caching สำหรับค่าที่ช้ากว่า แต่ที่นี่จะเพิ่มให้ทั้งคู่ โปรดทราบ ใน production ควรมี data points อย่างน้อยหลายร้อย จึงจะใช้ aggregate measure เช่น mean หรือ P90 เปรียบเทียบได้
เพิ่ม Upstash for Redis ใน Analytics Helper Code
มา看看โค้ดสำหรับเพิ่ม Upstash for Redis ใน analytics helper function Webmentions helper function คล้ายกัน ดูเต็มๆ ได้ใน GitHub repo (ลิงก์ด้านล่าง)
import { Redis } from "upstash/mod.ts";
const UPSTASH_REDIS_REST_TOKEN = Deno.env.get("UPSTASH_REDIS_REST_TOKEN");
if (typeof UPSTASH_REDIS_REST_TOKEN === "undefined") {
console.error("env `UPSTASH_REDIS_REST_TOKEN` must be set");
}
const UPSTASH_REDIS_REST_URL = Deno.env.get("UPSTASH_REDIS_REST_URL");
if (typeof UPSTASH_REDIS_REST_URL === "undefined") {
console.error("env `UPSTASH_REDIS_REST_URL` must be set");
}
const redis = new Redis({
token: UPSTASH_REDIS_REST_TOKEN,
url: UPSTASH_REDIS_REST_URL,
});
ก่อนอื่น ต้อง initialize Upstash for Redis object ต้องการค่า UPSTASH_REDIS_REST_TOKEN และ UPSTASH_REDIS_REST_URL จาก Upstash console เร็วมากถ้ายังไม่มีบัญชี Upstash ใส่ค่าทั้งสอง (UPSTASH_REDIS_REST_TOKEN และ UPSTASH_REDIS_REST_URL) ลงในไฟล์ .env ใน project root directory
สังเกตบรรทัดแรก ใช้ upstash key จาก import map ที่ตั้งค่าไว้ก่อนหน้า สะดวกกว่าเพิ่ม full import URL ในแต่ละ TypeScript file เมื่อ upstash_redis release ใหม่ จะต้องอัปเดต version number แค่ที่เดียว
นี่คือฟังก์ชัน getTinybirdViews:
export async function getTinybirdViews({ days, }: { days: number; }): Promise<number> {
try {
// ...TRUNCATED
const cachedCount = (await redis.get("view-count")) as number | null;
if (cachedCount != null) {
return cachedCount;
}
// ...TRUNCATED
const response = await fetch(
`https://api.tinybird.co/v0/pipes/${TINYBIRD_PIPE_NAME}.json?${params.toString()}`,
{
headers: {
Authorization: `Bearer ${TINYBIRD_TOKEN}`,
},
},
);
const { data: [{ count_sessions: count = -1 }], } = await response.json();
if (typeof count === "number" && count > 0) {
const CACHE_TTL_SECONDS = 14_400;
await redis.set("view-count", count);
await redis.expire("view-count", CACHE_TTL_SECONDS);
}
return count;
} catch (error: unknown) {
// ...TRUNCATED
}
}
ที่นี่เรา:
- เช็คว่ามี Upstash for Redis cached value สำหรับ
view-countแล้วหรือยัง ด้วยredis.get('view-count') - ถ้ามี คืนค่า cached value ถ้าไม่มี ดึงค่าใหม่จาก Tinybird
- เก็บและตั้ง expiry สำหรับค่าใหม่ใน Upstash for Redis cache โดย
redis.set('view-count', value)แล้วredis.expire(TTL)ตั้งค่าพร้อม time to live (TTL) หลังจากนั้นข้อมูลถือว่า stale เราตั้งTTL4 ชั่วโมง สำหรับการเรียกgetTinybirdViewsหลังช่วงนั้น จะ hit Tinybird ขอค่าใหม่

Refresh หน้า (หลายครั้ง) ด้วย Upstash for Redis integrated ในทั้งสอง data queries เร็วขึ้น จาก just over 2 seconds เหลือ around 0.29 seconds อย่าอ่านตัวเลขลึกเกินไป เพราะรัน locally และ data points ไม่มาก ลองใช้ service บนแอปของตัวเองดูได้
Upstash for Redis และ Performance API: สรุป
ที่นี่เราเห็นวิธีใช้ JavaScript Performance API ช่วยตัดสินใจว่าจะ focus optimization efforts ที่ไหน นอกจากนี้เห็นวิธี implement Upstash for Redis ใน Deno Fresh สุดท้ายค้นพบว่า Deno รองรับ Web APIs server side ทำให้ learning curve ราบรื่น อีกประโยชน์ใหญ่ของ Deno สำหรับ optimization คือ instant deploys ให้ short feedback loop ทำให้เร็วขึ้นตอน fine-tune performance
หวังว่าอ่านเกี่ยวกับ Upstash for Redis และ Performance API จะมีประโยชน์ ถ้าคุณใหม่กับ Deno หรือ Deno Fresh ดู content ที่ผมสร้างเกี่ยวกับ getting started with Deno ได้ โค้ดเต็มของแอปดูได้ที่ GitHub