ข้ามไปยังเนื้อหา

Upgrade และ Timing

เมื่อ 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(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 / await
async 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 ถัดไป

ก่อน 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;
}

ช่วงเวลาของ 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 ของตัวเอง

ดู element เริ่มต้นเป็น placeholder ธรรมดา จากนั้นเปลี่ยนเป็นสถานะที่ถูก upgrade หลังจาก 1.2 วินาที เมื่อ customElements.define ถูกเรียกภายใน setTimeout

ตัวเลือกBenefitCost
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 :defined pattern — เว็บไซต์จำนวนมากที่ใช้ custom elements ใช้ my-el:not(:defined) { visibility: hidden; } ร่วมกับ loading placeholder เพื่อป้องกัน Flash of Undefined Content ระหว่างรอ script โหลดและ define element

YouTube — custom elements บนหน้า YouTube ต้องรองรับกรณีที่ script โหลดแบบ async ทำให้ element ปรากฏใน DOM ก่อนที่ definition จะพร้อม การจัดการ upgrade timing อย่างถูกต้องจึงจำเป็นต่อการไม่ให้ UI พังหรือกระพริบ

"Upgrading" custom element หมายความว่าอะไร?
customElements.whenDefined(tagName) คืนค่าอะไร?
CSS pseudo-class :defined ให้คุณทำอะไรได้?