Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> JavaScript

Build Performant \"Get Viewport Dimensions\" Mini-App ด้วย JavaScript

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 resize event setup
  • innerWidth/innerHeight properties ดึง viewport dimensions
  • Callback functions เรียก function นอก function
  • Conditional if statements
  • setTimeout() delay code execution
  • clearTimeout() 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);
})();

CodePen play with it

How to Use on Any Project

Script tag bottom of HTML above :

<script src="/js/get-viewport-dimensions.js"></script>

Browser Compatibility

Modern browsers + IE9+

Advanced Throttling

Lodash JS library มี performance features เยอะ popular among devs เรียนรู้จาก library code ได้

Resources