Beginner-friendly practical Vanilla JS tutorial Takeaway: เขียน High-Performance Window Events ที่ไม่ kill user hardware
For: Complete Beginners to Intermediates
วันนี้สร้าง mini-app "Viewport Dimensions" แสดง browser viewport width/height ตอน resize window
Tool ไม่ควรมากับ responsive design CodePen มี feature คล้ายกัน แต่ผมต้องการอะไรมากกว่า จึงสร้างเอง เรียนรู้มากมายเกี่ยวกับ performance handling
Technical Topics:
- createElement() method สร้าง elements
- Style object สไตล์ elements
- Window Object
resizeevent setup - innerWidth/innerHeight properties ดึง viewport dimensions
- Callback functions เรียก function นอก function
- Conditional
ifstatements setTimeout()delay code executionclearTimeout()reset timer- Event throttler function ปรับปรุง performance window events
- ความสัมพันธ์ MS & FPS (Milliseconds & Frames Per Second)
- ความแตกต่าง Perceived Performance vs Hardware Performance
The Approach
สร้าง 2 versions:
- Version 1 ทำให้ app ทำงาน (draft)
- Version 2 ปรับปรุง v1: performant, energy efficient, readable
ทำทุกอย่างใน JS file เดียว เพื่อเพิ่มในโปรเจกต์อื่นได้ง่าย
Complete Beginner? ใช้ CodePen follow along ไม่ต้อง sign up ดู video tutorial
How to Learn
สับสน? ย้อนกลับอ่านซ้ำ ไม่ได้ skip สำคัญ เรียนรู้ดีกว่า嘗試แก้ปัญหาเองก่อน แล้วค่อยหา reference code
Lost? ดู finished code: Version 1 | Version 2
Viewport Dimensions [Version 1.0]
Requirements
- Container แสดง width & height
- ตำแหน่ง top-right corner
- Hidden by default
- Show ตอน resize window
- Hide 3 วินาทีหลังหยุด resize
สร้าง Display Container และ Style
สร้าง div element ด้วย createElement:
var viewportDimensions = document.createElement("div");
Style ด้วย Style object properties (inline style):
viewportDimensions.style.position = "fixed";
viewportDimensions.style.right = "0";
viewportDimensions.style.top = "0";
viewportDimensions.style.padding = "16px";
viewportDimensions.style.zIndex = "3";
viewportDimensions.style.fontSize = "22px";
Syntax นี้คือ dot notation ใช้ . เข้าถึง properties/methods ของ object
Append div เข้า body:
var body = document.querySelector("body");
body.appendChild(viewportDimensions);
appendChild() เพิ่ม node เข้า node อื่น ทุกอย่างใน DOM เป็น node
เพิ่ม border ชั่วคราวเช็คว่าทำงาน:
viewportDimensions.style.border = "10px solid";
Setup Window Resize Event
Window Object มี addEventListener listen events: scroll, click, type, resize
window.addEventListener("resize", function() {
// Code to execute when window resizes
});
Test ด้วย console.log("Working!") ใน function block resize window → console พิมพ์ "Working!" หลายครั้ง = working
Return Browser Width & Height
Window Object properties innerWidth innerHeight คืนค่า width/height ใน pixels (รวม scrollbars)
window.addEventListener("resize", function() {
var width = window.innerWidth;
var height = window.innerHeight;
console.log("Width:", width);
console.log("Height:", height);
});
Assign ให้ variables เพื่อใช้งานต่อ:
window.addEventListener("resize", function() {
var width = window.innerWidth;
var height = window.innerHeight;
viewportDimensions.textContent = width + "px" + " x " + height + "px";
});
textContent property ของ element object คืนค่า text node ใส่ค่า width/height ลงใน div
Countdown Timer with setTimeout()
ไม่อยากเห็น dimensions ตลอด ให้หาย 3 วินาทีหลังหยุด resize
ใช้ setTimeout(functionToExecute, 3000) — 3000ms = 3 วินาที
สร้าง function ลบ div:
function removeViewportDimensions() {
viewportDimensions.style.display = "none";
}
เรียกจากใน window event:
setTimeout(removeViewportDimensions, 3000);
ทุกครั้ง resize event active → setTimeout ตั้ง countdown 3 วินาที → เรียก removeViewportDimensions() → ซ่อน div
But We Have a Problem
Resize ครั้งที่ 2 หลัง div หาย → div ไม่ show อีก เพราะ display='none' ค้างอยู่ ไม่มี code บอกให้ visible อีก
ครั้งแรก visible เพราะ div default เป็น block-level element (display='block' visible)
Fix: ใส่ viewportDimensions.style.display = "block"; ใน resize event block (ก่อน setTimeout)
Another Problem: Flickering
Resize นาน > 3 วินาที → div กระพริบ (flicker) เหมือนไฟเปิดปิด
เพราะไม่ reset countdown timer ทุก resize → window event execute function หลายร้อยครั้ง → removeViewportDimensions() เรียกหลายครั้ง → overlapping execution → div ถูก hammer display='none' และ 'block' พร้อมกัน
Fix: ใช้ clearTimeout() clear existing timers
Resetting the Countdown Timer
clearTimeout() clear timer ที่ set ด้วย setTimeout — partner methods
ต้องใช้ global variable สร้าง timeout method
JS มี 2 scopes: global & local
- Global variable: declare นอก function
- Local variable: declare ใน function
สร้าง global variable resizeTimeout ที่ top ของ file:
var resizeTimeout;
Assign setTimeout() ให้ resizeTimeout:
resizeTimeout = setTimeout(removeViewportDimensions, 3000);
Clear timeout ก่อน set ใหม่:
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(removeViewportDimensions, 3000);
ตอนนี้ resize 5-6 วินาที flickering หาย!
How It Works
JS interpreter อ่าน code top-to-bottom line-by-line ทุก tiny resize fire new window event → execute code หลายร้อยครั้ง/วินาที
ทุกครั้ง clearTimeout() reset existing countdown ใน queue บน resizeTimeout
บรรทัดสุดท้าย assign setTimeout() → last thing = one 3-second countdown ก่อนหน้าถูก wipe out
Version 1 done ดู complete code → Version 2 focus on performance
Viewport Dimensions [Version 2]
App ทำงาน แต่พอดีไหม? พิจารณา 2 อย่าง:
- Perceived performance: UX
- Hardware performance: usage & efficiency
UX: app performs great fast response renders well even old screens
แต่ Hardware performance: really bad ใช้ CPU/GPU มากเกินไป relative to requirements
ปัญหา
Resize window → window event execute function hundreds of times in seconds
CodePen visualize ปัญหา — ดู function executions/second และ total count โตเร็ว
Horrible และ unnecessary! Code ใช้ resources มากเกินไป
ดู CPU/GPU usage ใน Chrome Task Manager resize window → ได้ % usage
Absolute vs Relative Hardware Usage
Absolute (total): tiny app ไม่ทำ harm คอมพิวเตอร์ท่วมท้น
Relative: horribly inefficient ไม่ควร neglect performance จะมากวนใจภายหลัง
Window Event Executes อย่างไร
Default: standard window event execute functions ให้มากที่สุดเท่าที่จะเป็นไปได้ ขณะ event active ทุก tiny change trigger new event
Execution rate: 4-40 milliseconds ขึ้นกับ hardware, browser, code complexity, other apps running
Hardware ใช้ resources ที่ available ในเวลานั้น
Hardware usage adds up → ต้อง fix เพราะ care about performance!
Modern browsers: minimum execution rate 4ms ≈ 250fps (Mozilla Web API docs)
Function ควร execute บ่อยแค่ไหน?
Depends on context: detailed animation? หรือ simple numeric values?
Generally ไม่อยากให้ window events execute มากที่สุด — absurd และไม่ฟรี ต้องหยุด!
Control execution frequency ด้วย throttling
Event Throttling to the Rescue!
Throttling function restrict จำนวนครั้ง function อื่น execute over time เช่น "call once per 100ms"
Basic Throttler Function:
var throttled;
var delay = 100;
function functionToExecute() {
console.log("Executed!");
}
function throttler() {
if (!throttled) {
functionToExecute();
throttled = true;
setTimeout(function() {
throttled = false;
}, delay);
}
}
window.addEventListener("resize", throttler);
CodePen test — functionToExecute เรียกได้ 10 ครั้ง/วินาที (10 x 100ms = 1000ms = 1s)
Throttler Code Explained:
Top of file: 2 global variables
var throttled; // undefined (falsy)
var delay = 100; // number
throttled reference point สำหรับ throttler() เช็ค queue
ก่อน throttler() trigger ครั้งแรก ไม่มี functions ใน queue → throttled = empty = undefined (falsy)
delay = 100ms regulation ส่งเข้า throttler's setTimeout() → run ทุก 100ms (10 times/sec max)
ใน throttler: conditional if (!throttled)
! = not → "ถ้า throttled ไม่มี truthy value → run if code" "ถ้า throttled มี truthy value → stop"
Truthy/Falsy Values in JS:
6 falsy values: undefined, null, 0, '', NaN, false — สิ่งอื่นๆ = truthy
Confirm throttled falsy:
var throttled;
console.log(throttled); // undefined
console.log(!!throttled); // false
ครั้งแรก throttler run → if condition met → enter if block
Inside if block
if (!throttled) {
functionToExecute(); // callback
throttled = true; // set true stop future calls
setTimeout(function() {
throttled = false; // reset back to false after delay
}, delay);
}
ทุกครั้ง resize event running → throttler() เรียก functionToExecute ทุก 100ms!
Throttling is Power
Test extremes ใน CodePen:
- delay 500ms → 2 calls/sec (infrequent)
- delay 16ms → 60 calls/sec (bombarded)
Throttling limit function calls over time Easy way avoid wasting hardware resources Hard part: หา "optimal" execution rate per project
Adding Throttler to Our Project
Refactor old code: 改善 structure ไม่เปลี่ยน functionality
ไม่อยากให้ resize event เรียก function โดยตรง → ให้ throttler regulate
Declare named function getViewportDimensions() ย้าย code จาก anonymous function:
function getViewportDimensions() {
var width = window.innerWidth;
var height = window.innerHeight;
viewportDimensions.textContent = width + "px" + " x " + height + "px";
viewportDimensions.style.display = "block";
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(removeViewportDimensions, 3000);
}
Throttler function ใหม่ เรียก getViewportDimensions():
function throttler() {
if (!throttled) {
getViewportDimensions();
throttled = true;
setTimeout(function() {
throttled = false;
}, delay);
}
}
Window resize event listener:
window.addEventListener("resize", throttler);
Hardware Performance vs Perceived Performance
Resize window → width/height numbers render ค่อนข้าง laggy ไม่ smooth เหมือนก่อนเพิ่ม throttler
Not surprising ก่อนเพิ่ม throttler execution rate absurdly high ตอนนี้ ~10 times/sec
Hardware perspective: very performant hardware hardly works → loves us
แต่ Perceived Performance (UX): ผู้ใช้รักเราไหม?
Rendering ควร smoother Execution rate capped at 10/sec → increase แต่เท่าไหร่?
Time vs Frame Rate
FPS (Frames Per Second) = frames displayed per second
Higher fps = smoother Video games: 60fps+ Movies/TV: 24-30fps Fast-paced = more FPS
บาง graphics 15fps smooth ได้ แต่น่าไป below 15fps → perceived performance slow
Target FPS in ms: 1000/target_fps → 1000/15 = 66ms
FPS to ms table:
var 2fps = 1000/2; // 500ms
var 10fps = 1000/10; // 100ms
var 15fps = 1000/15; // 66ms
var 24fps = 1000/24; // 42ms
var 30fps = 1000/30; // 33ms
var 60fps = 1000/60; // 16ms
App ปัจจุบัน 100ms = 10fps < 15fps → ดู slow
Testing
Guarantee: 15fps (66ms) smoother than 10fps ไม่ cost มาก
Test: 5fps (500ms), 15fps (66ms), 60fps (16ms)
5fps horrible broken hardware loves users hate
15fps good clearly better than 10fps ส่วนใหญ่คนจะคิดว่า smooth สำหรับ graphic ประเภทนี้
60fps super smooth แต่ไม่ worth extra hardware usage
Settle at 15fps
Why 15 FPS?
High enough perceived smooth (most people) most software/computers/displays handle 15fps แม้ handheld devices
Irony: high fps บางครั้ง opposite effect — force frame rate ที่ hardware ไม่รับไหว → lagging
Refresh rate: display redraw screen ครั้ง/วินาที 45Hz = 45 times/sec
CPU/GPU 60fps แต่ display 45Hz → mismatch → unpredictable results
Dev machines มัก powerful กว่า average user → ต้อง research และ test assumptions
15fps = happy medium works for non-powerful machines
15fps save ≥75% energy vs before throttler
Below 15fps usually slow/laggy obvious at ~10fps
Context & Minimum Effective Dose
Perceived difference 15 to 30fps? Test บน app → surprised ไม่ significant difference แต hardware cost double!
High fps costs hardware power Why use more than necessary? Point of diminishing returns = Minimum Effective Dose (MED) — ใช้แค่พอได้ effect ไม่มากไม่น้อย
Context matters: FPS video game vs simple pixel counter
Finishing Touch
Wrap code ใน IIFE (function () {...})(); + initialize use strict mode:
(function() {
"use strict";
// code here
})();
IIFE (Immediately-Invoked Function Expression) เก็บ code ใน local scope ป้องกัน "polluting global namespace" (conflicts กับ code ภายนอก)
use strict; ป้องกัน conflicts กับ 3rd party software ที่ไม่ follow strict rules
Finished Project Code
(function() {
"use strict";
var throttled;
var delay = 66; // 15fps
var resizeTimeout;
var body = document.querySelector("body");
var viewportDimensions = document.createElement("div");
viewportDimensions.style.position = "fixed";
viewportDimensions.style.right = 0;
viewportDimensions.style.top = 0;
viewportDimensions.style.padding = "16px";
viewportDimensions.style.zIndex = 3;
viewportDimensions.style.fontSize = "22px";
body.appendChild(viewportDimensions);
function getViewportDimensions() {
var width = window.innerWidth;
var height = window.innerHeight;
viewportDimensions.textContent = width + "px" + " x " + height + "px";
viewportDimensions.style.display = "block";
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(removeViewportDimensions, 3000);
}
function removeViewportDimensions() {
viewportDimensions.style.display = "none";
}
function throttler() {
if (!throttled) {
getViewportDimensions();
throttled = true;
setTimeout(function() {
throttled = false;
}, delay);
}
}
window.addEventListener("resize", throttler);
})();
How to Use on Any Project
Script tag bottom of HTML above