Promise.withResolvers() และ performance.now() · 2 ท่าเขียน JavaScript ที่ควรใช้แทนวิธีเดิมในปี 2026
รวม 2 กับดักการเขียน JavaScript แบบเดิมๆ พร้อมวิธีสลับมาใช้ Promise.withResolvers() และ performance.now() ที่สั้น กระชับ แม่นยำกว่าเดิม และช่วยให้รู้เท่าทันเวลา AI เจนโค้ดท่าเก่ามาให้

เห็นบรรทัด let resolve, reject; ลอยอยู่เหนือ new Promise(...) เมื่อไร นั่นคือสัญญาณว่าโค้ดจุดนั้นยังเป็นท่าเขียนแบบเดิม แม้โค้ดชุดนี้จะรันได้ ผ่านเทสต์ และขึ้น production ได้ตามปกติ แต่จริงๆ แล้ว JavaScript ยุคนี้มีวิธีเขียนที่สั้นกระชับและอ่านง่ายกว่าเดิมมาก
บทความนี้สรุป 2 กับดักคลาสสิกที่หลายคนยังเขียนติดมือ พร้อมแนวทางปรับให้ถูกต้องในปี 2026 เรื่องแรกคือ Promise.withResolvers() ตัวช่วยแก้ปัญหาการหนีออกจาก Promise constructor ส่วนเรื่องที่สองคือ performance.now() สำหรับใช้แทน Date.now() เวลาต้องการวัดระยะเวลา ทั้งสองฟีเจอร์กลายเป็นมาตรฐานมาระยะหนึ่งแล้ว และ AI ช่วยเขียนโค้ดก็มักแอบหยิบท่าเก่ามาให้ เพราะโมเดลเรียนรู้จากฐานโค้ดเดิมที่เคยเขียนกันมา พอเรารู้เท่าทันก็แก้กลับให้เป๊ะขึ้นได้ง่ายๆ ในบรรทัดเดียว
กับดักแรก: การ "หนีออก" จาก Promise constructor
Promise คือตัวแทนของงานแบบ asynchronous ที่ต้องรอผลลัพธ์ เช่น การรอข้อมูลจากเซิร์ฟเวอร์หรือรอเปิดการเชื่อมต่อ ปกติเวลาสร้างด้วย new Promise((resolve, reject) => {...}) จะได้ฟังก์ชัน resolve ไว้แจ้งงานสำเร็จ และ reject ไว้แจ้งงานล้มเหลว ซึ่งเรียกใช้ได้เฉพาะภายใน scope นั้นเท่านั้น
ปัญหาจะเกิดตอนที่จุดสั่งจบงานของ Promise ดันอยู่คนละที่กับจุดที่สร้าง เช่น ต้องรอ event จากข้างนอกมาสั่ง วิธีแก้ที่ทำกันมานานคือประกาศตัวแปร resolve กับ reject ทิ้งไว้ข้างนอกก่อน แล้วค่อยแอบดึงค่าจริงจากใน constructor ออกมาเก็บไว้ เพื่อให้หยิบไปเรียกใช้จากจุดไหนก็ได้ตามต้องการ แพตเทิร์นนี้เรียกว่า deferred หรือการสร้าง Promise ทิ้งไว้เพื่อรอสั่งจบงานในภายหลัง
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
แม้โค้ดรูปแบบนี้จะทำงานได้ตามปกติ แต่ก็ดูเปราะบางและอ่านยากโดยไม่จำเป็น เพราะต้องสร้างตัวแปรลอยทิ้งไว้เป็นค่าว่าง แล้วค่อยมาใส่ค่าจริงเมื่อโค้ดใน constructor ทำงาน คนมาอ่านโค้ดต่อต้องคอยไล่สายตาขึ้นลงเพื่อดูว่าโค้ดตั้งใจทำอะไร แถมถ้าเผลอลืมเขียนบรรทัด resolve = res ไปแค่นิดเดียว โค้ดจะพังแบบเงียบๆ ทันที นี่คือจุดที่ Parsa Jiravand ชี้ไว้ในบทความบน dev.to ว่าเป็นท่าที่คนพยายามหนีออกจากกรอบของ Promise constructor มาโดยตลอด
Promise.withResolvers() เขียนสิ่งเดียวกันในบรรทัดเดียว

ตั้งแต่มาตรฐาน JavaScript ปี 2024 (ES2024) เป็นต้นมา ได้เพิ่มวิธีเขียนมาตรฐานที่สั้นกระชับกว่าเดิม นั่นคือ Promise.withResolvers() ที่จะคืนค่าเป็น object พร้อม promise, resolve และ reject มาให้ครบชุด สามารถ destructure ออกมาใช้งานได้ทันที
const { promise, resolve, reject } = Promise.withResolvers();
เขียนจบได้ในบรรทัดเดียว โดยไม่ต้องประกาศตัวแปรว่างทิ้งไว้ และไม่ต้องคอยดึงค่าออกจาก callback แถมยังสื่อเจตนาชัดเจนตั้งแต่บรรทัดแรกว่ากำลังเตรียม Promise ชนิด deferred ไว้เพื่อรอสั่งจบงานจากภายนอก จุดสำคัญคือพฤติกรรมของทั้งสองท่าเหมือนกันทุกอย่าง ทั้งจังหวะทำงานของ microtask การส่งต่อ error ไปจนถึงการเรียกใช้ .then และ .catch ต่างกันแค่ความกระชับและความชัดเจนของโค้ด โดยเรียก resolve() หรือ reject() จากนอกฟังก์ชันได้ตามปกติ เช่น กรณีเขียนครอบ API ระบบ event อย่างการรอ WebSocket เปิดการเชื่อมต่อ หรือรอจับ error ก็ช่วยให้โค้ดสั้นลงโดย logic หลักยังเหมือนเดิม
ด้านการรองรับ ปัจจุบัน Promise.withResolvers() จัดอยู่ในระดับมาตรฐานทั่วไป (Baseline 2024) ซึ่งหยิบมาใช้งานได้ทันทีโดยไม่ต้องลง polyfill เพิ่มเติม หาก environment ที่รันรองรับเวอร์ชันตามนี้
| ตัวรัน | เวอร์ชันที่รองรับ |
|---|---|
| Chrome | 119 |
| Firefox | 121 |
| Safari | 17.4 |
| Node.js | 22 |
ฝั่ง TypeScript เริ่มรองรับมาตั้งแต่รุ่น 5.4 ซึ่งจะ return ค่าเป็น type PromiseWithResolvers<T> แต่ถ้า target ในโครงการยังตั้งไว้เป็นเวอร์ชันเก่ากว่า ก็สามารถเพิ่ม "ES2024" เข้าไปใน lib ของไฟล์ tsconfig.json ได้
อย่างไรก็ตาม ไม่ใช่ทุกสถานการณ์ที่จำเป็นต้องหยิบ Promise.withResolvers() มาใช้ หากเราควบคุมทั้งจุดสร้างและจุดสั่งจบงานได้ในจุดเดียว เช่น งานที่ใช้ await รับส่งค่าตรงๆ อย่าง fetch หรือ fs.readFile การใช้ async/await หรือ new Promise() แบบปกติจะเหมาะสมกว่า รูปแบบ deferred เหมาะเฉพาะกรณีที่จุดสร้างและจุดสั่งจบงานอยู่คนละตำแหน่งจนแชร์ callback ร่วมกันไม่ได้โดยตรง ส่วนอีกจุดที่ต้องระวังคือ หากมีการเรียก resolve() ทันทีแบบ synchronous ภายในฟังก์ชันตัวเดียวกับที่สร้างมันขึ้นมา แสดงว่าสิ่งที่ต้องการจริงๆ คือ Promise.resolve(value) ซึ่งไม่จำเป็นต้องใช้ withResolvers เลย
กับดักที่สอง: วัดเวลาด้วย Date.now()
กับดักต่อมาอาจดูเป็นคนละเรื่อง แต่เกิดจากต้นเหตุเดียวกัน นั่นคือการนำเครื่องมือยุคเก่ามาใช้กับงานที่ไม่ได้ถูกออกแบบมาโดยตรง เวลาที่เราอยากคำนวณว่าระบบใช้เวลาทำงานไปนานเท่าไร วิธีที่เห็นกันบ่อยคือจับเวลาด้วย Date.now() ตอนเริ่มต้น แล้วเรียกจับเวลาอีกรอบตอนทำงานเสร็จ จากนั้นนำค่าทั้งสองมาลบกัน
Date.now() จะคืนค่าเป็นจำนวน millisecond ที่นับตั้งแต่ epoch หรือจุดเริ่มต้นเวลามาตรฐานตั้งแต่ช่วงเที่ยงคืนของวันที่ 1 มกราคม ปี 1970 ตามเวลา UTC ฟังดูเหมือนไม่มีปัญหา แต่จริงๆ แล้ว Date ไม่ใช่นาฬิกาที่เดินหน้าไปทางเดียว เพราะหน้าที่หลักของมันคือการบอกวันและเวลาสำหรับมนุษย์ ระบบปฏิบัติการจึงต้องมีระบบซิงค์เวลาอัตโนมัติอย่าง NTP คอยคุมให้เวลาของเครื่องตรงกับเวลาจริงเสมอ ซึ่งการปรับเวลานี้ขยับได้ทั้งเดินหน้าและถอยหลังครั้งละหลาย millisecond และเกิดขึ้นได้หลายครั้งภายในหนึ่งชั่วโมง
ผลที่ตามมาคือ หากระบบปฏิบัติการปรับเวลาถอยหลังระหว่างที่เรากำลังจับเวลา ผลต่างที่คำนวณได้จะคลาดเคลื่อนทันที และถ้าถูกปรับถอยหลังมากพอ ระยะเวลาที่วัดได้ก็อาจกลายเป็นติดลบทั้งที่เวลาเดินไปข้างหน้าตามปกติ เพราะกลายเป็นว่าเรากำลังนำค่าจากนาฬิกาคนละเรือนมาลบกัน
performance.now() นาฬิกาที่ไม่มีวันเดินถอยหลัง

วิธีที่ถูกต้องสำหรับการวัดระยะเวลาคือการใช้ performance.now() จาก Performance API ซึ่งเป็นนาฬิกาประเภท monotonic ค่าที่อ่านได้ครั้งถัดไปจะเท่ากับหรือมากกว่าค่าก่อนหน้าเสมอ โดยไม่ได้รับผลกระทบจากการซิงค์ปรับเวลาของระบบ ด้วยเหตุนี้ MDN Web Docs จึงระบุไว้อย่างชัดเจนว่าควรใช้ performance.now() แทน Date.now() สำหรับงานวัด performance การคำนวณเฟรมต่อวินาที (FPS) การทำ animation loop รวมไปถึงงานที่ต้องการคำนวณระยะเวลาการทำงาน
การใช้งานตรงไปตรงมา แค่จับค่าเวลาก่อนและหลังเริ่มงาน แล้วนำมาหาผลต่าง
const t0 = performance.now();
// …งานที่ต้องวัดระยะเวลา…
const t1 = performance.now();
const duration = t1 - t0; // ระยะเวลาที่ไม่เพี้ยนจากการซิงค์นาฬิกา
สิ่งที่มักทำให้สับสนบ่อยๆ คือค่าที่ performance.now() คืนกลับมาไม่ได้นับจาก epoch เหมือน Date.now() แต่นับอ้างอิงจาก timeOrigin ซึ่งก็คือจุดเริ่มต้นของหน้านั้น (กรณีเว็บเพจคือจุดที่เริ่มโหลดหน้า) ทำให้ตัวเลขทั้งสองแบบมีจุดอ้างอิงต่างกันโดยสิ้นเชิง
Date.now(); // 1678889977578 (นับจาก epoch)
performance.now(); // 233936 (นับจาก timeOrigin ไม่ใช่ epoch)
จะเห็นว่าค่าของ performance.now() มีขนาดเล็กกว่ามาก เพราะเริ่มนับตั้งแต่ตอนที่หน้าเว็บเริ่มทำงาน ไม่ใช่นับย้อนไปถึงปี 1970 นี่จึงเป็นเหตุผลที่มันเหมาะกับการวัดความยาวของช่วงเวลามากกว่าการระบุเวลาปัจจุบัน
ภาษาโตขึ้นแล้ว
จุดเชื่อมโยงของทั้งสองเรื่องนี้ ไม่ใช่ว่าวิธีเขียนแบบเดิมผิดมาตั้งแต่แรก เพราะทั้งการแกะ resolve ออกมาจาก Promise constructor และการจับเวลาด้วย Date.now() ต่างเคยเป็นทางเลือกเดียวในยุคที่ภาษายังไม่มีฟีเจอร์ที่ตอบโจทย์โดยตรง แต่สิ่งที่เปลี่ยนไปคือวันนี้ JavaScript มีทั้ง Promise.withResolvers() และ performance.now() ที่ช่วยให้ทำสิ่งเดิมได้สั้นกระชับและแม่นยำยิ่งขึ้น
ประเด็นที่น่าสนใจในยุคนี้คือ เครื่องมือ AI ช่วยเขียนโค้ดยังคงหยิบวิธีเขียนแบบเก่ามาให้เห็นอยู่บ่อยๆ เนื่องจากโมเดลถูกฝึกฝนมาจากฐานโค้ดเก่าจำนวนมาก การอ่านโค้ดให้ออกและรู้เท่าทันจึงเป็นทักษะสำคัญที่ไม่ใช่แค่ปล่อยให้ AI เจนโค้ดออกมาแล้วก๊อปปี้ไปใช้ต่อทันที ซึ่งนี่คือประเด็นเรื่องการตัดสินใจที่ vibe coding มักพังเพราะขาดไป
ครั้งต่อไปถ้าคุณสังเกตเห็น let resolve ลอยอยู่เหนือ new Promise หรือเห็นการคำนวณระยะเวลาผ่าน Date.now() ในโค้ดที่คุณหรือ AI เขียนขึ้นมา นั่นคือสัญญาณว่าโค้ดตรงนั้นยังเป็นวิธีเขียนยุคเก่า และตอนนี้คุณสามารถปรับให้ถูกต้องและทันสมัยขึ้นได้ง่ายๆ เพียงบรรทัดเดียว
ที่มา:
- บทความ You keep escaping the Promise constructor. Promise.withResolvers() does it right. จาก dev.to โดย Parsa Jiravand
- เอกสาร High precision timing (Performance API) จาก MDN Web Docs
ชอบเรื่องแนวนี้ มีอีบุ๊คฟรีให้อ่านต่อ
Vibe Coding สำหรับคนไม่ใช่โปรแกรมเมอร์ ใช้ Claude Code สร้าง landing page, mini app และ prototype จริงโดยไม่ต้องเขียนโค้ด
กดสมัครแล้วเราจะส่งเทคนิค AI และของแจกใหม่ๆ ให้ทางอีเมล เลิกรับได้ตลอด
Vibecoding · The Developer's Playbook

ฉบับภาษาไทย 10 บท พา dev สร้าง Personal Finance Tracker (LINE OA + AI จัดหมวดอัตโนมัติ) ตั้งแต่โครงโปรเจกต์บรรทัดแรกจนแอปทำงานจริงบน server


