Vorax Online: รายงานวิเคราะห์ FPS ฉบับเต็ม
ทดสอบบน Windows วันที่ 2 พ.ย. 2026 · Electron 44.4.5, three.js r186 · Ryzen 9 9950X3D, RTX 4090, จอ 2560×1440 279 Hz
สรุป
คอขวดอยู่ที่ เธรดหลักของ JavaScript ไม่ใช่การ์ดจอ
เน็ต หรือตัว Electron สาเหตุใหญ่ที่สุดคือเกมมี
requestAnimationFrame แยกกันราว 11 loop และ loop
เหล่านี้ไม่ได้ฟัง cap FPS หรือโหมดประหยัดพลังงาน ตั้ง cap ไว้
เกมก็ยังทำงานเกือบเต็มคอร์
ตอนเกมอยู่หน้าสุด เครื่องทดสอบได้ FPS กลาง 113 และแทบไม่ต่ำกว่า 60 ส่วน FPS 30 ที่เห็นบ่อยเกิดจาก cap ตอนหน้าต่างไม่อยู่หน้าสุด ไม่ใช่เครื่องไม่ไหว ปัญหาจึงไม่ได้อยู่ที่เกมช้าในฉากปกติ แต่อยู่ที่เธรดหลักแทบไม่เหลือแรงสำรอง พอเจอคนเยอะ บอส หรือเครื่องที่ CPU ช้ากว่า FPS จะร่วงทันที
ทำไม FPS ถึงตก
-
Cap FPS ไม่ได้ลดงานทั้งเกม: cap
ทำงานด้วยการข้ามเฟรมใน loop หลัก
aoผ่านCaแต่ rAF ยังถูกเรียกตามจอ 279 Hz และ loop อื่นทำงานทุกครั้ง จึงเห็น rAF ราว 256 ครั้งต่อวินาทีทั้งที่จอเกมแสดงราว 100 FPS -
FPS 30 พอดีคือ cap: เกมตั้ง cap 30
ตอนหน้าต่างไม่อยู่หน้าสุด วัดได้ 172 จาก 176 ตัวอย่างอยู่ที่ 30 FPS
และ 5 วินาทีต่อมาเข้าโหมด
idleLite - เธรดหลักไม่มีแรงสำรอง: ใช้ไปกับ matrix, การวาดข้อความ, DOM, animation และ loop ย่อย เมื่อคนหรือเอฟเฟกต์เพิ่มขึ้น เวลาต่อเฟรมจะเกินงบทันที
- ยังมีเฟรมกระตุกหนักเป็นครั้งคราว: พบ 8 ครั้งที่นาน 221–1,025 ms ระหว่าง 18 นาที ยังไม่รู้สาเหตุแน่ชัด
วิธีทดสอบ
| การทดสอบ | รายละเอียด |
|---|---|
| Client จริง 18 นาที | อ่าน FPS บนจอและ metric ทุก 2 วินาที รวม 535 ตัวอย่าง และบันทึกสถานะโฟกัสในรอบหลัง |
| Profile ใน Electron | อัด 15 ช่วง ช่วงละ 10 วินาที โดยอัดอัตโนมัติเมื่อ FPS ต่ำกว่า 60 |
| Profile ใน Chrome | อัด 2 รอบ รวมราว 67 วินาที และนับคำสั่ง WebGL 45 วินาที |
| อ่านโค้ด JS ของเกม | Setting, loop, การวาด, ตัวละคร, effect, UI, network และหน่วยความจำ |
| Process จากภายนอก | CPU ต่อเธรด, GPU, VRAM, เน็ต, ดิสก์ และ flag ของ Electron |
ฉากทดสอบคือการเล่นด้วย AUTO ค่า setting: quality high, cap 120,
แสดงผู้เล่น 10 คน, fxOthers ปิด ชื่อใน minified code
อ้างตามไฟล์ที่ส่งจริง ชื่อในซอร์สอาจต่างกัน
ผลวัด
FPS และเธรดหลัก (รอบที่บันทึกโฟกัส 8 นาที)
| ค่าที่วัด | อยู่หน้าสุด (62 ตัวอย่าง) | ไม่อยู่หน้าสุด (176 ตัวอย่าง) |
|---|---|---|
| FPS กลาง | 113 | 30 |
| FPS ต่ำ (p5) / ต่ำสุด | 71 / 30 | 30 / 15 |
| ตัวอย่างที่ต่ำกว่า 60 FPS | 2 | 175 |
| เฟรมช้าสุด (p95 / สูงสุด) | 36 / 46 ms | 36 / 286 ms |
| เธรดหลักไม่ว่าง | 93.6% | 60.3% |
| เวลา Script | 60.6% | 23.9% |
| Style / Layout | 4.2% / 2.2% | 6.3% / 3.0% |
สิ่งที่บอกว่า loop แยกกันทำงานไม่หยุด
| ค่าต่อวินาที (จาก profile) | อยู่หน้าสุด ~100 FPS | ไม่อยู่หน้าสุด 30 FPS |
|---|---|---|
| Callback ของ rAF | 1,559 | 2,567 |
| เวลาใน rAF | 677 ms | 299 ms |
| Paint (ครั้ง) | 470 | 936 |
| Layerize (เวลา) | 45 ms | 78 ms |
| Style + Layout + Paint + Layerize + Commit (เวลา) | ~208 ms | ~267 ms |
ตอน cap 30 FPS เกมกลับเรียก rAF และ Paint มากขึ้น งาน DOM ก็ไม่ลดลงตาม FPS ค่าหน้าสุดมาจากช่วง profile เดียว ใช้ดูแนวโน้ม
เวลาเธรดหลักตอนอยู่หน้าสุด (ค่าจาก Chrome)
บางกลุ่มซ้อนกัน จึงรวมกันไม่เท่า 100% ใน Electron ช่วงที่วัดได้
updateMatrixWorld สูง 8.59%,
drawOverlay 5.62%, getParameters 3.33%
ภาระการวาด
| คำสั่ง | ต่อวินาที | ต่อเฟรม (ที่ 95 FPS) |
|---|---|---|
| Draw call | 33,358 | ~350 |
| สามเหลี่ยม | 75 ล้าน | ~790,000 |
useProgram |
15,108 | ~160 |
uniformMatrix4fv |
45,244 | ~480 |
bindFramebuffer |
2,030 | ~21 |
bufferSubData |
2,352 (35.8 MB) | ~25 (0.38 MB) |
หน่วยความจำ
| ค่าที่วัด | รอบ 10 นาที | รอบ 8 นาที |
|---|---|---|
| JS Heap สูงสุด | 625 MB | 337 MB |
| DOM node สูงสุด (รวมที่ค้างรอ GC) | 91,488 | 87,761 |
| Event listener สูงสุด | 14,548 | 7,023 |
Node แกว่งจากราว 9,000 ถึงเกือบ 90,000 ทั้งที่หน้าเกมมีจริงราว 3,500 น่าจะมีการสร้าง DOM แล้วทิ้งเป็นจำนวนมาก ค่าจบรอบกลับลงมาใกล้เดิม จึงยังไม่ใช่หลักฐานของ leak
Electron เทียบ Chrome
ตอนอยู่หน้าสุดค่าหลักเท่ากันในระยะไม่เกินราว 1 จุด (เธรดหลัก 97–98%, GPU ~15%) เปลี่ยน wrapper อย่างเดียวจึงไม่น่าทำให้ FPS ดีขึ้น
สิ่งที่ควรแก้
เรียงตามผลต่อ FPS ที่คาดไว้ ระดับผลกระทบประเมินจากโค้ดและ profile ยังไม่ได้แก้จริงแล้ววัด
-
รวม loop ทั้งหมดเข้า loop หลัก
ผลสูงยืนยันในโค้ดและวัดแล้วยากปานกลาง- หลักฐาน
-
มี loop ทำงานตลอด 11 ตัว:
ao, arenagatek, firenesth, gatherdt, gathertoolsq, islandpropsy, lifeskillX, outfitfxl, petsce, redgateR, gachaktและ titles เพิ่มอีก 1 ตัวต่อ canvas ไม่มีตัวไหนเช็ก cap หรือpowerSaveหลายตัวเรียกprojเขียนstyle.left/topและ arenagate ตั้งinnerHTMLทุก tick - วิธีแก้
-
เพิ่มรายการ
onFrame(fn)แล้วเรียกจากVeหลังCaผ่าน เขียน DOM เฉพาะตอนค่าเปลี่ยน - ข้อดี
- Cap และโหมดประหยัดพลังงานจะได้ผลจริง และลดงาน DOM ตามไปด้วย
- ข้อควรระวัง
- งานที่ต้องวิ่งตอนซ่อนหน้าต่าง เช่น logic ต้องอยู่นอก rAF ตามเดิม
-
เปลี่ยนค่าเริ่มต้น cap เป็น 60
ผลสูงยืนยันในโค้ดง่ายมาก- หลักฐาน
- ค่าเริ่มต้น desktop คือ 120 ตัวเลือกมี auto/30/60/120
- ข้อดี
- เฟรมที่วาดลดลงครึ่งหนึ่ง CPU และความร้อนลดลงตาม ผู้เล่นเลือกกลับเป็น 120 ได้
- ข้อควรระวัง
- ควรทำพร้อมข้อ 1 ไม่งั้น loop อื่นยังวิ่งตามจอเหมือนเดิม
-
เลิกวาดข้อความสดทุกเฟรมใน drawOverlay
ผลสูงยืนยันแล้วยากปานกลาง- หลักฐาน
-
drawOverlay5.62% และset fontสูงสุด 4.73% helpernตั้ง font แล้ววาด stroke + fill ของทุกตัว,proj.okเช็กแค่ z ป้ายนอกจอจึงยังวาด, loop NPC ไม่เช็กokเลย ตัวเลขดาเมจสร้าง font ใหม่ทุกเฟรมตอนย่อขยาย และ crit ใช้shadowBlur14 - วิธีแก้
-
แคชป้ายลง offscreen canvas แล้ว
drawImage, ทำ digit atlas, เช็กขอบจอแกน x/y, เรียงตาม font เพื่อลดการตั้งค่า - ข้อดี
- ได้ผลมากตอนตีกันเยอะ ซึ่งเป็นจุดที่ FPS ตก
- ข้อควรระวัง
- ใช้หน่วยความจำเพิ่ม ชื่อไทยมีสระและวรรณยุกต์ซ้อน ต้องแคชทั้งชื่อ และต้องล้างแคชตอนเปลี่ยนขนาดฟอนต์
-
ลดการคำนวณ matrix
ผลสูงยืนยันแล้วยากปานกลาง- หลักฐาน
-
ราว 17% ของเธรดหลัก
matrixAutoUpdate=falseมีจุดเดียวในearthking.js,fxb.syncเรียกupdateMatrixWorld(true)ต่อ effect ต่อเฟรม,pickMonsterเรียกprojสองครั้งต่อมอนทุกเฟรม - วิธีแก้
- แช่ matrix ของของนิ่งในแมพ, ทำ hover pick เฉพาะตอนเมาส์ขยับหรือ 20 Hz, อัปเดต root ของ effect ครั้งเดียว
- ข้อดี
- ลดงานทุกเฟรม เห็นผลชัดในแมพที่มีของเยอะ
- ข้อควรระวัง
- ของที่ขยับต้องเรียกอัปเดตเอง ถ้าลืมวัตถุจะค้างตำแหน่ง
-
จำกัด animation ของมอนและ NPC
ผลสูงยืนยันในโค้ดยากต่ำถึงกลาง- หลักฐาน
-
syncEntitiesอัปเดตมอนบนจอทุกเฟรม, NPC อัปเดตเสมอ, ผู้เล่นไกลอัปเดตทุก 3 เฟรม,animateRigreset ทุกข้อต่อก่อนทุกครั้ง, จำกัดผู้เล่นได้ แต่มอนและ NPC ไม่มีตัวจำกัด - วิธีแก้
- ตัวไกลอัปเดต 15–20 Hz, ใช้กฎเดียวกันกับมอนและ NPC, เพิ่มเพดานจำนวน rig
- ข้อดี
- ได้ผลตรงกับฉากคนเยอะและมอนเยอะ
- ข้อควรระวัง
- ตัวไกลขยับไม่ลื่นเท่าตัวใกล้ แต่เป็นผลด้านภาพเท่านั้น เพราะอยู่บนเซิร์ฟเวอร์
-
ลด pass ของ post-processing
ผลกลางยืนยันในโค้ดง่าย- หลักฐาน
-
มีการสลับ render target 18 ครั้งต่อเฟรม: shadow 1, render 1,
bloom 13, tilt 2, output 1 เมื่อตั้ง
bloomK=0 pass ยังทำงานต่อ - วิธีแก้
- ปิด pass เมื่อ strength เป็น 0, ลด bloom เหลือ 3 mip, ถอด tilt ใน preset ต่ำ
- ข้อดี
- ลดงานทั้ง CPU และ GPU โดยเฉพาะการ์ดจอออนบอร์ด
- ข้อควรระวัง
- ภาพเปลี่ยน ควรเก็บ preset สูงไว้ให้คนชอบภาพสวย
-
ปรับค่า renderer ที่เสียเปล่า
ผลกลางยืนยันในโค้ดง่าย- หลักฐาน
-
antialias:trueไม่มีผลกับฉาก เพราะ composer target ไม่มีsamples,PCFSoftShadowMapถูกถอดใน r186 จึงเป็น PCF, เงา 2048 วาดใหม่ทุกเฟรมตามกล้อง - วิธีแก้
-
ตั้ง
antialias:falseหรือเพิ่มsamplesให้ composer และอัปเดตเงาทุก 2–3 เฟรมเมื่อกล้องไม่ขยับมาก - ข้อดี
- แก้ง่ายและได้แรงคืนทันที
- ข้อควรระวัง
- เงาตัวละครที่เคลื่อนไหวอาจดูหน่วงเล็กน้อย
-
Pool material ของ effect
ผลกลางยืนยันในโค้ดยากปานกลาง- หลักฐาน
-
makeFxสร้าง material ใหม่ต่อ effect ทำให้getParametersและgetProgramโผล่ระหว่างเล่น (3.33% ในช่วงที่วัด) ไม่พบneedsUpdateทุกเฟรม ส่วนการที่ material outlineReใช้ร่วมระหว่าง skinned กับ static อาจทำให้สลับ program บ่อย (สันนิษฐาน) - วิธีแก้
-
Pool ตามชนิด effect, แยก
Reเป็น 2 ตัว, เพิ่มเงื่อนไขให้ effect เข้า batch ได้มากขึ้น - ข้อดี
- ลดงานตอนสกิลเยอะ
- ข้อควรระวัง
- ต้อง reset ค่าใน pool ให้สะอาด ไม่งั้น effect จะมีสีหรือค่าค้าง
-
ลดการอัปโหลด buffer
ผลกลางที่มายังไม่ครบยากปานกลาง- หลักฐาน
-
วัดได้ 35.8 MB/s และพบการอัปโหลดทั้ง buffer ใน
wings.js,wings-cyber.js,fashion.js,pets.js,redgate.js,gacha.jsและ portal แต่ขนาดที่อ่านได้น้อยกว่าที่วัด จึงน่าจะมีแหล่งอื่นอีก - วิธีแก้
-
ใช้
addUpdateRange, ข้ามตัวนอกจอ, แชร์ pool ต่อชนิด - ข้อดี
- ลดงาน GPU process ที่ใช้ CPU สูงอยู่
- ข้อควรระวัง
- ควรหาแหล่งใหญ่ให้เจอก่อน ไม่งั้นอาจแก้แล้วไม่เห็นผล
-
ลด line of sight และงานใน Jn
ผลกลางยืนยันในโค้ดยากปานกลาง- หลักฐาน
-
Jnทำงานทุกเฟรมที่วาด และWeตรวจ line of sight ซ้ำตามเส้นทาง - วิธีแก้
- รัน 20–30 Hz แล้ว interpolate ตำแหน่งที่เหลือ
- ข้อดี
- เกมเดินเท่าเดิมทุก FPS และลดงานเธรดหลัก
- ข้อควรระวัง
- ตัวละครตัวเองต้องยังตอบสนองทันที ไม่ควรรู้สึกหน่วง
-
เปลี่ยนข้อมูล network เป็น binary
ผลขึ้นกับจำนวนคนยืนยันในโค้ดยากสูง- หลักฐาน
-
ใช้ JSON ผ่าน WebSocket, snapshot ส่งข้อมูลเต็มต่อ entity
และใส่ค่าทันทีที่รับ,
TICK_RATE20 - วิธีแก้
- ส่งเฉพาะค่าที่เปลี่ยน, ใช้ binary, parse ใน Worker
- ข้อดี
- ลดเน็ตและ CPU ตอนคนเยอะ ทั้งฝั่ง client และ server
- ข้อควรระวัง
- ต้องแก้ทั้ง client และ server ควรทำเมื่อ profile ฉากคนเยอะยืนยันว่าจำเป็น
-
เช็ก DOM churn และเฟรมกระตุกหนัก
สันนิษฐานต้องสืบต่อ- หลักฐาน
-
Node ขึ้นถึง 91,488, heap ถึง 625 MB และพบเฟรม 221–1,025 ms
รวม 8 ครั้ง GC ปกติแค่ราว 0.4–0.5%
จึงต้องดูว่าการกระตุกเกิดจาก GC ก้อนใหญ่หรืองานอื่น พบ
rainObjsไม่ถูก dispose ตอนถอด และaddEventListener269 จุด เทียบกับremoveEventListener22 จุด - วิธีแก้
-
เปิด DevTools Memory เทียบ heap snapshot, ติด
longtaskobserver และ log ฉากที่เกิดเหตุ - ข้อดี
- สำคัญกับผู้เล่นที่เปิด AUTO ทิ้งไว้นาน
- ข้อควรระวัง
- อย่าตัดสินว่าเป็น leak จาก Node อย่างเดียว เพราะค่าจบรอบกลับมาใกล้เดิม
-
ลดงานเล็กที่รันทุกเฟรม
ผลต่ำยืนยันในโค้ดง่าย- หลักฐาน
-
c.updateใช้querySelector3 ครั้ง และJSON.stringify2 ครั้งทุกเฟรม,buildFrameห่อtexImage2Dใน production, telemetry ทุก 60 วินาทีเรียกscene.traverse - วิธีแก้
-
จำกัด
c.updateที่ 10 Hz, ปิดตัวห่อใน production - ข้อดี
- แก้เร็ว ความเสี่ยงต่ำ
- ข้อควรระวัง
- HP ตัวเองและคูลดาวน์ต้องยังตอบสนองทันที
Setting ในเกม
| Setting | ตัวเลือก | ค่าเริ่มต้น | ข้อสังเกต |
|---|---|---|---|
quality |
high, mid, low | high (มือถือ mid) | high: pixel ratio 1.75, เงา 2048, bloom, tilt · low: 0.75, ไม่มีเงา bloom tilt |
fpsCap |
auto, 30, 60, 120 | 120 | auto คือ 60 บน desktop และ 30 บน touch ไม่มีตัวเลือกไม่จำกัด |
dynRes |
เปิด/ปิด | ปิด | ถูกบังคับปิดตอนโหลด ตัวปรับความละเอียดจึงแทบไม่ทำงาน |
showPlayers |
0, 5, 10, 20 | 10 | ไม่มีตัวเลือกเดียวกันสำหรับมอนหรือ NPC |
fxOthers |
เปิด/ปิด | ปิด | ปิดไว้แล้วช่วยได้มากตอนคนเยอะ |
powerSave |
เปิด/ปิด | ปิด | ซ่อน canvas และรัน logic ใน Worker 50 ms แต่ loop แยกยังทำงาน |
-
ไม่อยู่หน้าสุด: cap 30 แล้วเข้า
idleLiteหลัง 5 วินาที ซ่อนผู้เล่นอื่นและ overlay - ไม่มี setting: ระยะมองเห็น, จำนวน particle, จำนวนมอน
- แนะนำให้ผู้เล่นตอนนี้: ถ้าเปิด AUTO ทิ้งไว้ ให้ใช้โหมดประหยัดพลังงาน และเล่นปกติให้ลอง cap 60 กับ quality mid
ลำดับงานและผลที่คาด
ระยะที่ 1: แก้เร็ว
- Cap เริ่มต้น 60
- ตั้ง
antialias:false - ปิด pass เมื่อ strength เป็น 0
c.update10 Hz- เช็กขอบจอให้ป้ายชื่อ
ระยะที่ 2: ผลมากสุด
- รวม loop เข้า
onFrame - แคชป้ายชื่อและ digit atlas
- แช่ matrix ของของนิ่ง
- จำกัด animation มอนและ NPC
ระยะที่ 3: งานใหญ่
- Pool material
- ลด buffer upload
- Binary network
- พิจารณา Web Worker
- เครื่องแรง ฉากปกติ: FPS อาจเท่าเดิม แต่ CPU และความร้อนลดลง
- เมือง บอส สงครามกิลด์: น่าจะดีขึ้นชัดที่สุด FPS ร่วงและกระตุกน้อยลง
- เครื่องทั่วไปและโน้ตบุ๊ก: น่าจะได้ประโยชน์มากกว่าเครื่องที่ทดสอบ
- ผู้เล่นเปิด AUTO ทิ้งไว้: ภาระเครื่องน่าจะลดลงมาก เพราะ loop ย่อยจะหยุดตาม cap
ควรวัดซ้ำในฉากเดิมทุกครั้งหลังแก้ และเปิดให้ผู้เล่นกลุ่มเล็กลองก่อน
งานเสริม
-
เปิด
dynResที่ถูกบังคับปิด แล้วเพิ่มตัวเลือกจำนวน particle และมอน -
แสดง
renderer.infoแบบ debug และครอบระบบด้วยperformance.mark() -
ใช้
renderer.compileAsync()ตอนโหลด และใช้ KTX2 กับ Meshopt -
ใช้
reload()แทนreloadIgnoringCache()และเพิ่ม service worker แคช asset - เพิ่มตัวติดตั้งปกติ เพราะ Portable แตกไฟล์ 385 MB ลง Temp ทุกครั้ง
-
เปิด fuse
OnlyLoadAppFromAsarกับEnableEmbeddedAsarIntegrityValidationและปิดRunAsNode
สิ่งที่ไม่ใช่ปัญหา
- การ์ดจอ: RTX 4090 ใช้แค่ราว 15% และเลือกการ์ดถูกตัว
- Electron: ตอนอยู่หน้าสุดผลเท่ากับ Chrome
- เน็ตและดิสก์: network service กิน CPU ไม่เกิน 0.7%
- GC ปกติ: ราว 0.4–0.5% ของเวลาเธรดหลัก
-
การส่งไฟล์: HTTP/3, gzip และแคช
immutableทำงานดี -
needsUpdate: ไม่พบการตั้งทุกเฟรมในโค้ดที่อ่าน
ข้อจำกัด
- ทดสอบตอนเล่นด้วย AUTO ยังไม่ได้วัดบอสโลก เมืองคนเยอะ หรือสงครามกิลด์โดยตรง
- ช่วง profile ตอนอยู่หน้าสุดใน Electron มีช่วงเดียว และเวลาทดสอบส่วนใหญ่หน้าต่างไม่ได้อยู่หน้าสุด
- เครื่องทดสอบสเปกสูงมาก เครื่องทั่วไปอาจมีคอขวดที่การ์ดจอเพิ่มด้วย
-
เครื่องมือวัดเพิ่มภาระเอง ค่าต่อเฟรมเป็นค่าประมาณที่ 95 FPS และ
(program)ราว 30–40% ในผล profile รวมงาน native หลายแบบ แยกละเอียดไม่ได้ -
ไม่ได้อ่านโค้ดฝั่ง server และ
googleauth.jsยังไม่รู้ขนาดและความถี่ของ snapshot จริง - ระดับผลกระทบเป็นการประเมิน ต้องยืนยันด้วยการแก้จริงแล้ววัดซ้ำ