Upgrade และ Timing
“Upgrade” คืออะไร
หัวข้อที่มีชื่อว่า ““Upgrade” คืออะไร”เมื่อ browser parse HTML อาจพบ tag เช่น <my-el> ก่อนที่ JavaScript จะทำงาน ณ จุดนั้น browser ยังไม่มี constructor ที่ลงทะเบียนสำหรับ my-el จึงสร้าง element นั้นเป็น HTMLElement ธรรมดา — เรียกว่า unknown element ที่ยังไม่มีพฤติกรรมพิเศษ
ต่อมาเมื่อ script เรียก customElements.define('my-el', MyEl) browser จะ upgrade: จะวนหา element ทุกตัวที่ตรงกับ tag นั้นในเอกสาร แล้วเรียก constructor ของ MyEl กับแต่ละตัว จากนั้นเรียก connectedCallback หาก element นั้นอยู่ใน DOM อยู่แล้ว
หมายความว่า upgrade เกิดได้สองลำดับ:
- Define ก่อน parse —
<script>ใน<head>ที่ใช้deferทำงานก่อนที่ parser จะถึง<my-el>element จะถูก construct ทันทีเมื่อ parser พบ tag นั้น - Define หลัง parse — script ที่โหลดแบบ lazy ทำงานหลังจาก HTML อยู่ใน DOM แล้ว ทุก
<my-el>ที่มีอยู่ในเอกสารจะถูก upgrade ณ ขณะที่defineถูกเรียก
ทั้งสองลำดับนี้เป็นสิ่งที่ตั้งใจออกแบบมาและรองรับอย่างสมบูรณ์ สิ่งที่ ไม่ควร ทำคือการสมมติว่า element มีพฤติกรรม custom ก่อนที่ define จะถูกเรียก
รอด้วย customElements.whenDefined
หัวข้อที่มีชื่อว่า “รอด้วย customElements.whenDefined”customElements.whenDefined(tagName) คืน Promise ที่ resolve เป็น class constructor ทันทีที่ tag name นั้นได้รับการลงทะเบียน ใช้เพื่อเลื่อนโค้ดที่ต้องพึ่งพา interface ของ element ที่ถูก upgrade แล้ว:
// แบบ Promise / .then()customElements.whenDefined('my-el').then(function(Ctor) { console.log('my-el is now defined:', Ctor); document.querySelector('my-el').doSomething();});// แบบ async / awaitasync function init() { const Ctor = await customElements.whenDefined('my-el'); console.log('my-el is now defined:', Ctor); document.querySelector('my-el').doSomething();}init();หาก element ถูก define ไปแล้วตอนที่เรียก whenDefined Promise จะ resolve ทันทีใน microtask tick ถัดไป
CSS pseudo-class :defined
หัวข้อที่มีชื่อว่า “CSS pseudo-class :defined”ก่อน upgrade custom element จะตรงกับ :not(:defined) ใน CSS หลังจาก customElements.define ถูกเรียกและการ upgrade เสร็จสิ้น element จะตรงกับ :defined ที่เป็น hook ที่สะอาดสำหรับซ่อน element ที่ยัง render ไม่สมบูรณ์จนกว่าจะพร้อม — ป้องกัน Flash of Undefined Content (FOUC):
my-el:not(:defined) { visibility: hidden;}pattern การป้องกัน FOUC ที่สมบูรณ์ยิ่งขึ้นซึ่งแสดง loading placeholder ด้วย:
<style> upgrade-demo:not(:defined) { visibility: hidden; }
upgrade-demo:defined { display: block; border: 2px solid #059669; }</style>
<upgrade-demo>Loading…</upgrade-demo>/* เพิ่ม animation การเปิดเผยได้ */upgrade-demo { transition: opacity 0.3s ease;}upgrade-demo:not(:defined) { opacity: 0;}upgrade-demo:defined { opacity: 1;}ข้อควรระวังเรื่อง Ordering
หัวข้อที่มีชื่อว่า “ข้อควรระวังเรื่อง Ordering”ช่วงเวลาของ define เทียบกับการ parse DOM มีผลสำคัญ:
- Script ใน
<head>โดยไม่มีdefer— script จะบล็อก parsing ทำให้defineถูกเรียกก่อน element ใดๆ ใน body จะถูก parse element จะถูก construct ทันทีที่ parser เจอ - Script ใน
<head>ที่มีdefer— script ทำงานหลังจาก document ถูก parse ครบ ทุก element ที่อยู่ใน DOM จะถูก upgrade พร้อมกันเมื่อdefineถูกเรียก - Script ที่ถูก import แบบ dynamic หรือโหลดแบบ lazy — เหมือน
defer: element ที่มีอยู่ทั้งหมดจะถูก upgrade เป็น batch เมื่อถึงเวลาdefine - Element ที่เพิ่มผ่าน JavaScript หลังจาก
define— จะถูก upgrade แบบ synchronous เมื่อถูกใส่เข้าไปใน document
ควรใช้ customElements.whenDefined เสมอ (หรือ customElements.upgrade(element) สำหรับ node เดี่ยว) เมื่อต้องการให้แน่ใจว่า element พร้อมก่อนที่จะเข้าถึง custom API ของตัวเอง
Demo แบบ Runnable
หัวข้อที่มีชื่อว่า “Demo แบบ Runnable”ดู element เริ่มต้นเป็น placeholder ธรรมดา จากนั้นเปลี่ยนเป็นสถานะที่ถูก upgrade หลังจาก 1.2 วินาที เมื่อ customElements.define ถูกเรียกภายใน setTimeout
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
Define ก่อน parse (script บล็อกใน <head>) | Element ถูก construct และ upgrade ทันทีที่ parser พบ tag ไม่มีช่วง “unknown element” ให้เห็น | Script บล็อก parsing ทำให้หน้าเว็บ render ช้าลง โดยเฉพาะถ้า bundle ใหญ่ |
Define หลัง parse (defer หรือโหลดแบบ lazy) | ไม่บล็อก initial render, โหลด script ได้ตามความจำเป็น | มีช่วงเวลาที่ element เป็น unknown element (ไม่มีพฤติกรรม, ไม่มี style พิเศษ) ต้องจัดการ FOUC เอง เช่นด้วย :not(:defined) |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- เข้าถึง custom API ของ element ก่อนเช็คว่า upgrade แล้วหรือยัง — ถ้าเรียก method หรืออ่าน property เฉพาะของ custom element ทันทีหลัง query โดยไม่รอ
customElements.whenDefined()อาจได้ element ที่ยังเป็นHTMLElementธรรมดาและไม่มี method นั้นอยู่ ทำให้เกิดTypeError - ไม่จัดการ FOUC ด้วย
:not(:defined)— ถ้าไม่ซ่อน element ที่ยังไม่ถูก upgrade ผู้ใช้จะเห็น raw content หรือ placeholder ที่ไม่มี style ชั่วครู่ก่อนที่ script จะโหลดเสร็จ ทำให้ UI กระพริบ - สมมติว่า element ทุกตัวถูก upgrade พร้อมกันเสมอ — ถ้ามีการเพิ่ม element ใหม่เข้า DOM ผ่าน JavaScript หลังจาก
defineไปแล้ว element นั้นจะถูก upgrade แบบ synchronous ทันที แต่ element ที่มีอยู่ก่อนdefineจะถูก upgrade เป็น batch — การไม่เข้าใจความต่างนี้ทำให้ debug timing bug ได้ยาก
💡 ตัวอย่างจากของจริง
CSS
:definedpattern — เว็บไซต์จำนวนมากที่ใช้ custom elements ใช้my-el:not(:defined) { visibility: hidden; }ร่วมกับ loading placeholder เพื่อป้องกัน Flash of Undefined Content ระหว่างรอ script โหลดและ define elementYouTube — custom elements บนหน้า YouTube ต้องรองรับกรณีที่ script โหลดแบบ async ทำให้ element ปรากฏใน DOM ก่อนที่ definition จะพร้อม การจัดการ upgrade timing อย่างถูกต้องจึงจำเป็นต่อการไม่ให้ UI พังหรือกระพริบ