API วงจรการใช้งานหน้าเว็บ

การรองรับเบราว์เซอร์

  • Chrome: 68
  • ขอบ: 79
  • Firefox: ไม่รองรับ
  • Safari: ไม่รองรับ

บางครั้งเบราว์เซอร์สมัยใหม่จะระงับหน้าเว็บหรือทิ้งหน้าเว็บทั้งหมดเมื่อทรัพยากรของระบบมีจํากัด ในอนาคต เบราว์เซอร์ต้องการดำเนินการนี้ในเชิงรุก จึงใช้พลังงานและหน่วยความจำน้อยลง Page Lifecycle API มีฮุกวงจรเพื่อให้หน้าเว็บจัดการการแทรกแซงของเบราว์เซอร์เหล่านี้ได้อย่างปลอดภัยโดยไม่ส่งผลกระทบต่อประสบการณ์ของผู้ใช้ ลองดู API เพื่อดูว่าคุณควรใช้ฟีเจอร์เหล่านี้ในแอปพลิเคชันหรือไม่

ข้อมูลเบื้องต้น

วงจรของแอปพลิเคชันเป็นวิธีหลักที่ระบบปฏิบัติการสมัยใหม่จัดการทรัพยากร ใน Android, iOS และ Windows เวอร์ชันล่าสุด ระบบปฏิบัติการสามารถเริ่มและหยุดแอปได้ทุกเมื่อ ซึ่งช่วยให้แพลตฟอร์มเหล่านี้ปรับปรุงและจัดสรรทรัพยากรใหม่ในลักษณะที่เป็นประโยชน์ต่อผู้ใช้มากที่สุด

ที่ผ่านมาบนเว็บไม่มีวงจรดังกล่าวและแอปสามารถทำงานได้อย่างต่อเนื่อง เนื่องจากมีหน้าเว็บจำนวนมากที่ทำงานอยู่ ทรัพยากรที่สำคัญของระบบ เช่น หน่วยความจำ, CPU, แบตเตอรี่ และเครือข่ายอาจสมัครใช้บริการมากเกินไป ซึ่งทำให้ผู้ใช้ปลายทางได้รับประสบการณ์ที่ไม่ดี

แม้ว่าแพลตฟอร์มเว็บจะมีเหตุการณ์ที่เกี่ยวข้องกับสถานะวงจรการใช้งานมาอย่างยาวนานแล้ว เช่น load, unload และ visibilitychange แต่เหตุการณ์เหล่านี้ช่วยให้นักพัฒนาแอปตอบสนองต่อการเปลี่ยนแปลงสถานะวงจรการใช้งานที่ผู้ใช้เป็นผู้เริ่มเท่านั้น เพื่อให้เว็บทำงานได้อย่างเสถียรบนอุปกรณ์ที่ใช้พลังงานต่ำ (และคำนึงถึงทรัพยากรโดยทั่วไปบนทุกแพลตฟอร์ม) เบราว์เซอร์ต้องมีวิธีเชิงรุกในการเรียกคืนและจัดสรรทรัพยากรของระบบใหม่

อันที่จริงแล้ว เบราว์เซอร์ในปัจจุบันใช้มาตรการต่างๆ เพื่อประหยัดทรัพยากรสำหรับหน้าในแท็บที่ทำงานอยู่เบื้องหลังอยู่แล้ว และเบราว์เซอร์หลายตัว (โดยเฉพาะ Chrome) ต้องการที่จะใช้มาตรการเหล่านี้มากขึ้นเพื่อลดการใช้ทรัพยากรโดยรวม

ปัญหาคือนักพัฒนาแอปไม่มีวิธีเตรียมพร้อมรับการแทรกแซงประเภทนี้ที่ระบบเป็นผู้เริ่ม หรือแม้แต่ไม่รู้ตัวว่ากำลังเกิดขึ้น ซึ่งหมายความว่า เบราว์เซอร์ต้องระมัดระวังอยู่เสมอ มิฉะนั้นอาจเสี่ยงต่อการทำให้หน้าเว็บเสียหาย

Page Lifecycle API จะพยายามแก้ปัญหานี้ด้วยวิธีต่อไปนี้

  • การนําเสนอและกำหนดมาตรฐานแนวคิดสถานะวงจรบนเว็บ
  • การกําหนดสถานะใหม่ซึ่งเริ่มต้นโดยระบบที่อนุญาตให้เบราว์เซอร์จํากัดทรัพยากรที่แท็บที่ซ่อนอยู่หรือไม่ใช้งานอาจใช้
  • การสร้าง API และเหตุการณ์ใหม่ที่ช่วยให้นักพัฒนาเว็บตอบสนองต่อการเปลี่ยนจากสถานะหนึ่งไปยังอีกสถานะหนึ่งซึ่งระบบเป็นผู้เริ่ม

โซลูชันนี้ช่วยให้นักพัฒนาเว็บคาดการณ์ได้ว่าจะสร้างแอปพลิเคชันที่ทนต่อการแทรกแซงของระบบได้อย่างไร และช่วยให้เบราว์เซอร์เพิ่มประสิทธิภาพทรัพยากรของระบบได้มากขึ้น ซึ่งท้ายที่สุดแล้วจะเป็นประโยชน์ต่อผู้ใช้เว็บทุกคน

ส่วนที่เหลือของโพสต์นี้จะแนะนำฟีเจอร์ใหม่เกี่ยวกับวงจรของหน้า และสำรวจความเกี่ยวข้องกับสถานะและเหตุการณ์ของแพลตฟอร์มเว็บที่มีอยู่ทั้งหมด นอกจากนี้ ยังมีคําแนะนําและแนวทางปฏิบัติแนะนําสําหรับประเภทงานที่นักพัฒนาแอปควร (และไม่ควร) ทําในแต่ละรัฐ

ภาพรวมสถานะและเหตุการณ์ในวงจรของหน้า

สถานะวงจรชีวิตของหน้าเว็บทั้งหมดจะแยกกันและแยกกันอยู่ กล่าวคือ หน้าเว็บจะอยู่ในสถานะใดสถานะหนึ่งได้เพียงสถานะเดียว และการเปลี่ยนแปลงส่วนใหญ่ในสถานะวงจรชีวิตของหน้าเว็บมักจะสังเกตได้ผ่านเหตุการณ์ DOM (ดูข้อยกเว้นในคําแนะนําสําหรับนักพัฒนาซอฟต์แวร์สําหรับสถานะแต่ละสถานะ)

วิธีอธิบายสถานะวงจรชีวิตของหน้าเว็บ รวมถึงเหตุการณ์ที่ส่งสัญญาณการเปลี่ยนสถานะระหว่างสถานะต่างๆ ได้ง่ายที่สุดคือการใช้แผนภาพ ดังนี้

ภาพสถานะและโฟลว์เหตุการณ์ตามที่อธิบายไว้ในเอกสารนี้
สถานะและลำดับเหตุการณ์ของ Page Lifecycle API

รัฐ

ตารางต่อไปนี้จะอธิบายสถานะแต่ละรายการโดยละเอียด รวมถึงแสดงสถานะต่างๆ ที่เป็นไปได้ซึ่งอาจเกิดขึ้นก่อนและหลัง รวมถึงเหตุการณ์ที่นักพัฒนาแอปสามารถใช้เพื่อสังเกตการเปลี่ยนแปลง

รัฐ คำอธิบาย
ใช้งานอยู่

หน้าเว็บจะอยู่ในสถานะใช้งานอยู่หากมองเห็นได้และมีโฟกัสอินพุต

สถานะก่อนหน้าที่เป็นไปได้:
passive (ผ่านเหตุการณ์ focus)
frozen (ผ่านเหตุการณ์ resume แล้วตามด้วยเหตุการณ์ pageshow)

สถานะถัดไปที่เป็นไปได้มีดังนี้
เชิงรับ (ผ่านเหตุการณ์ blur)

เชิงรับ

หน้าเว็บจะอยู่ในสถานะพาสซีฟหากมองเห็นได้และไม่มีโฟกัสอินพุต

สถานะก่อนหน้าที่เป็นไปได้มีดังนี้
ใช้งานอยู่ (ผ่านเหตุการณ์ blur)
ซ่อนอยู่ (ผ่าน visibilitychange event)
frozen (ผ่านเหตุการณ์ resume จากนั้น 2}/20}{202)pageshow

สถานะถัดไปที่เป็นไปได้:
ทำงานอยู่ (ผ่านเหตุการณ์ focus)
ซ่อนอยู่ (ผ่านเหตุการณ์ visibilitychange)

ซ่อน

หน้าเว็บจะอยู่ในสถานะซ่อนหากมองไม่เห็น (และไม่ได้ถูกตรึง ทิ้ง หรือสิ้นสุด)

สถานะก่อนหน้าที่เป็นไปได้มีดังนี้
พาสซีฟ (ผ่านเหตุการณ์ visibilitychange)
frozen (ผ่านเหตุการณ์ resume ตามด้วยเหตุการณ์ pageshow)

สถานะถัดไปที่เป็นไปได้:
passive (ผ่านเหตุการณ์ visibilitychange)
frozen (ผ่านเหตุการณ์ freeze)
discarded (ไม่มีเหตุการณ์ที่เริ่มทํางาน)
terminated (ไม่มีเหตุการณ์ที่เริ่มทํางาน)

หยุดการทำงาน

ในสถานะหยุดทำงาน เบราว์เซอร์จะระงับการดำเนินการของงานที่คิวงานของหน้าเว็บจนกว่าหน้าเว็บจะเลิกหยุดทำงาน ซึ่งหมายความว่าสิ่งต่างๆ เช่น ตัวจับเวลา JavaScript และ Callback ของ Fetch จะไม่ทํางาน งานที่กำลังทำงานอยู่จะทำงานเสร็จ (ที่สำคัญที่สุดคือ callbacks freeze) แต่อาจถูกจำกัดในสิ่งที่ทำได้และระยะเวลาที่ทำงานได้

เบราว์เซอร์จะตรึงหน้าเว็บไว้เพื่อประหยัดการใช้ CPU/แบตเตอรี่/อินเทอร์เน็ต และเพื่อทำให้ การไปยังส่วนต่างๆ แบบย้อนกลับ/ไปข้างหน้าที่เร็วขึ้นได้ เพื่อหลีกเลี่ยงไม่ให้ต้องโหลดหน้าทั้งหน้าซ้ำ

สถานะก่อนหน้าที่เป็นไปได้:
hidden (ผ่านเหตุการณ์ freeze)

สถานะถัดไปที่เป็นไปได้:
ทำงานอยู่ (ผ่านเหตุการณ์ resume แล้วตามด้วยเหตุการณ์ pageshow)
ทำงานอยู่แบบพาสซีฟ (ผ่านเหตุการณ์ resume แล้วตามด้วยเหตุการณ์ pageshow)
ซ่อนอยู่ (ผ่านเหตุการณ์ resume)
ทิ้ง (ไม่มีเหตุการณ์ที่เริ่มทํางาน)

สิ้นสุดการใช้งาน

หน้าเว็บจะอยู่ในสถานะสิ้นสุดเมื่อเบราว์เซอร์เริ่มยกเลิกการโหลดและล้างหน้าเว็บออกจากหน่วยความจํา สถานะนี้จะเริ่มงานใหม่ไม่ได้ และระบบอาจหยุดงานที่กำลังดำเนินการอยู่หากทำงานนานเกินไป

สถานะก่อนหน้าที่เป็นไปได้:
hidden (ผ่านเหตุการณ์ pagehide)

สถานะถัดไปที่เป็นไปได้:
ไม่มี

ทิ้งแล้ว

หน้าเว็บจะอยู่ในสถานะถูกละทิ้งเมื่อเบราว์เซอร์ยกเลิกการโหลดหน้าเว็บเพื่อประหยัดทรัพยากร จะไม่มีงาน การเรียกกลับเหตุการณ์ หรือ JavaScript ทุกประเภทในสถานะนี้ เนื่องจากการยกเลิกมักเกิดขึ้นภายใต้ข้อจำกัดของทรัพยากร ซึ่งไม่สามารถเริ่มต้นกระบวนการใหม่ได้

ในสถานะทิ้ง ผู้ใช้มักจะเห็นแท็บนั้นเอง (รวมถึงชื่อแท็บและ Favicon) แม้ว่าหน้าเว็บจะหายไปแล้วก็ตาม

สถานะก่อนหน้าที่เป็นไปได้:
hidden (no events fired)
frozen (no events fired)

สถานะถัดไปที่เป็นไปได้มีดังนี้
ไม่มี

กิจกรรม

เบราว์เซอร์จะส่งเหตุการณ์จำนวนมาก แต่มีเพียงส่วนน้อยที่บ่งบอกถึงการเปลี่ยนแปลงที่เป็นไปได้ในสถานะวงจรชีวิตของหน้าเว็บ ตารางต่อไปนี้แสดงเหตุการณ์ทั้งหมดที่เกี่ยวข้องกับวงจรของลูกค้าและสถานะที่เหตุการณ์อาจเปลี่ยนจากสถานะหนึ่งไปยังอีกสถานะหนึ่ง

ชื่อ รายละเอียด
focus

องค์ประกอบ DOM ได้รับการโฟกัส

หมายเหตุ: เหตุการณ์ focus ไม่จําเป็นต้องเป็นสัญญาณการเปลี่ยนสถานะ โดยจะส่งสัญญาณการเปลี่ยนแปลงสถานะก็ต่อเมื่อหน้าเว็บไม่มีโฟกัสอินพุตก่อนหน้านี้

สถานะก่อนหน้าที่เป็นไปได้:
passive

สถานะปัจจุบันที่เป็นไปได้:
ใช้งานอยู่

blur

องค์ประกอบ DOM เสียโฟกัส

หมายเหตุ: เหตุการณ์ blur ไม่ได้บ่งบอกถึงการเปลี่ยนแปลงสถานะเสมอไป โดยจะส่งสัญญาณการเปลี่ยนแปลงสถานะในกรณีที่หน้าเว็บไม่มีโฟกัสอินพุตอีกต่อไป (เช่น หน้าเว็บไม่ได้เปลี่ยนโฟกัสจากองค์ประกอบหนึ่งไปยังอีกองค์ประกอบหนึ่ง)

สถานะก่อนหน้าที่เป็นไปได้:
ใช้งานอยู่

สถานะปัจจุบันที่เป็นไปได้:
passive

visibilitychange

ค่า visibilityState ของเอกสารมีการเปลี่ยนแปลง ซึ่งอาจเกิดขึ้นเมื่อผู้ใช้ไปยังหน้าใหม่ สลับแท็บ ปิดแท็บ ย่อหรือปิดเบราว์เซอร์ หรือสลับแอปในระบบปฏิบัติการบนอุปกรณ์เคลื่อนที่

สถานะก่อนหน้าที่เป็นไปได้:
passive
hidden

สถานะปัจจุบันที่เป็นไปได้มีดังนี้
พาสซีฟ
ซ่อนอยู่

freeze *

หน้าเว็บเพิ่งหยุดทำงาน ระบบจะไม่เริ่มงานใดๆ ที่หยุดชั่วคราวได้ในคิวงานของหน้า

สถานะก่อนหน้าที่เป็นไปได้:
ซ่อนอยู่

สถานะปัจจุบันที่เป็นไปได้:
frozen

resume *

เบราว์เซอร์กลับมาแสดงหน้าเว็บที่ค้างอีกครั้ง

สถานะก่อนหน้าที่เป็นไปได้มีดังนี้
ค้าง

สถานะปัจจุบันที่เป็นไปได้มีดังนี้
ใช้งานอยู่ (หากตามด้วยเหตุการณ์ pageshow)
พาสซีฟ (หากตามด้วยเหตุการณ์ pageshow)
ซ่อนอยู่

pageshow

กําลังไปยังรายการประวัติเซสชัน

ซึ่งอาจเป็นการโหลดหน้าเว็บใหม่ล่าสุดหรือหน้าที่นำมาจาก Back/Forward Cache หากหน้าเว็บมาจากแคชย้อนหลัง/ไปข้างหน้า พร็อพเพอร์ตี้ persisted ของเหตุการณ์จะเป็น true มิเช่นนั้นจะเป็น false

สถานะก่อนหน้าที่เป็นไปได้:
frozen (เหตุการณ์ resume จะทริกเกอร์ด้วย)

สถานะปัจจุบันที่เป็นไปได้:
ทำงานอยู่
ทำงานอยู่แบบพาสซีฟ
ซ่อนอยู่

pagehide

กำลังข้ามผ่านรายการประวัติเซสชัน

หากผู้ใช้ไปที่หน้าอื่นและเบราว์เซอร์สามารถเพิ่มหน้าปัจจุบันลงในแคชย้อนหลังเพื่อนำกลับมาใช้ได้ในภายหลังได้ พร็อพเพอร์ตี้ persisted ของเหตุการณ์จะเป็น true เมื่อ true หน้าเว็บจะเข้าสู่สถานะค้าง ไม่เช่นนั้นจะเข้าสู่สถานะยุติ

สถานะก่อนหน้าที่เป็นไปได้:
hidden

สถานะปัจจุบันที่เป็นไปได้:
frozen (event.persisted is true, freeze event follows)
terminated (event.persisted is false, unload event follows)

beforeunload

ระบบกำลังจะยกเลิกการโหลดหน้าต่าง เอกสาร และทรัพยากรของเอกสาร เอกสารจะยังคงปรากฏอยู่และยังยกเลิกกิจกรรมได้ในตอนนี้

สำคัญ: ควรใช้เหตุการณ์ beforeunload เพื่อแจ้งให้ผู้ใช้ทราบถึงการเปลี่ยนแปลงที่ไม่ได้บันทึกไว้เท่านั้น เมื่อบันทึกการเปลี่ยนแปลงแล้ว ระบบควรนำเหตุการณ์ออก คุณไม่ควรเพิ่ม URL อย่างไม่มีเงื่อนไขลงในหน้าเว็บ เนื่องจากอาจทำให้ประสิทธิภาพลดลงในบางกรณี ดูรายละเอียดได้ที่ส่วน API รุ่นเดิม

สถานะก่อนหน้าที่เป็นไปได้:
ซ่อนอยู่

สถานะปัจจุบันที่เป็นไปได้:
terminated

unload

หน้าเว็บกำลังยกเลิกการโหลด

คำเตือน: เราไม่แนะนำให้ใช้เหตุการณ์ unload เนื่องจากไม่น่าเชื่อถือและอาจส่งผลต่อประสิทธิภาพในบางกรณี ดูรายละเอียดเพิ่มเติมได้ที่ส่วน API รุ่นเดิม

สถานะก่อนหน้าที่เป็นไปได้:
hidden

สถานะปัจจุบันที่เป็นไปได้:
terminated

* ระบุเหตุการณ์ใหม่ที่กำหนดโดย Page Lifecycle API

ฟีเจอร์ใหม่ที่เพิ่มเข้ามาใน Chrome 68

แผนภูมิก่อนหน้าแสดงสถานะ 2 สถานะที่ระบบเป็นผู้เริ่มดำเนินการ ไม่ใช่ผู้ใช้ ได้แก่ หยุดชั่วคราวและทิ้ง ดังที่ได้กล่าวไว้ก่อนหน้านี้ เบราว์เซอร์ในปัจจุบันจะค้างและทิ้งแท็บที่ซ่อนอยู่เป็นครั้งคราว (ตามการพิจารณาของเบราว์เซอร์) แต่นักพัฒนาแอปจะไม่ทราบเวลาที่จะเกิดเหตุการณ์นี้

ใน Chrome 68 ตอนนี้นักพัฒนาซอฟต์แวร์สามารถสังเกตได้ว่าแท็บที่ซ่อนอยู่ถูกหยุดทำงานและเลิกหยุดทำงานเมื่อใดโดยฟังเหตุการณ์ freeze และ resume ใน document

document.addEventListener('freeze', (event) => {
  // The page is now frozen.
});

document.addEventListener('resume', (event) => {
  // The page has been unfrozen.
});

ใน Chrome 68 ตอนนี้ออบเจ็กต์ document มีพร็อพเพอร์ตี้ wasDiscarded ใน Chrome บนเดสก์ท็อป (ระบบจะติดตามการรองรับ Android ในปัญหานี้) หากต้องการดูว่าหน้าเว็บถูกทิ้งขณะที่อยู่ในแท็บที่ซ่อนอยู่หรือไม่ คุณสามารถตรวจสอบค่าของพร็อพเพอร์ตี้นี้เมื่อโหลดหน้าเว็บ (หมายเหตุ: ต้องโหลดหน้าที่ถูกยกเลิกอีกครั้งเพื่อใช้งานอีกครั้ง)

if (document.wasDiscarded) {
  // Page was previously discarded by the browser while in a hidden tab.
}

ดูคําแนะนําเกี่ยวกับสิ่งที่ควรทําในเหตุการณ์ freeze และ resume รวมถึงวิธีจัดการและเตรียมพร้อมสําหรับการทิ้งหน้าเว็บได้ที่คําแนะนําสําหรับนักพัฒนาซอฟต์แวร์สําหรับสถานะแต่ละสถานะ

ส่วนต่อๆ ไปจะเสนอภาพรวมว่าฟีเจอร์ใหม่เหล่านี้เหมาะกับสถานะและเหตุการณ์ของแพลตฟอร์มเว็บที่มีอยู่อย่างไร

วิธีสังเกตสถานะอายุการใช้งานของหน้าเว็บในโค้ด

ในสถานะแอ็กทีฟ แพสซีฟ และซ่อน คุณจะเรียกใช้โค้ด JavaScript ที่ระบุสถานะวงจรของหน้าปัจจุบันจาก API ของแพลตฟอร์มเว็บที่มีอยู่ได้

const getState = () => {
  if (document.visibilityState === 'hidden') {
    return 'hidden';
  }
  if (document.hasFocus()) {
    return 'active';
  }
  return 'passive';
};

ในทางกลับกัน สถานะหยุดชั่วคราวและสิ้นสุดจะตรวจพบได้ใน Listener เหตุการณ์ที่เกี่ยวข้องเท่านั้น (freeze และ pagehide) เนื่องจากสถานะมีการเปลี่ยนแปลง

วิธีสังเกตการเปลี่ยนแปลงสถานะ

ต่อยอดจากฟังก์ชัน getState() ที่กำหนดไว้ก่อนหน้านี้ คุณสามารถดูการเปลี่ยนแปลงสถานะอายุการใช้งานของหน้าทั้งหมดได้ด้วยโค้ดต่อไปนี้

// Stores the initial state using the `getState()` function (defined above).
let state = getState();

// Accepts a next state and, if there's been a state change, logs the
// change to the console. It also updates the `state` value defined above.
const logStateChange = (nextState) => {
  const prevState = state;
  if (nextState !== prevState) {
    console.log(`State change: ${prevState} >>> ${nextState}`);
    state = nextState;
  }
};

// Options used for all event listeners.
const opts = {capture: true};

// These lifecycle events can all use the same listener to observe state
// changes (they call the `getState()` function to determine the next state).
['pageshow', 'focus', 'blur', 'visibilitychange', 'resume'].forEach((type) => {
  window.addEventListener(type, () => logStateChange(getState()), opts);
});

// The next two listeners, on the other hand, can determine the next
// state from the event itself.
window.addEventListener('freeze', () => {
  // In the freeze event, the next state is always frozen.
  logStateChange('frozen');
}, opts);

window.addEventListener('pagehide', (event) => {
  // If the event's persisted property is `true` the page is about
  // to enter the back/forward cache, which is also in the frozen state.
  // If the event's persisted property is not `true` the page is
  // about to be unloaded.
  logStateChange(event.persisted ? 'frozen' : 'terminated');
}, opts);

โค้ดนี้ทํา 3 สิ่งต่อไปนี้

  • ตั้งค่าสถานะเริ่มต้นโดยใช้ฟังก์ชัน getState()
  • กำหนดฟังก์ชันที่ยอมรับสถานะถัดไป และหากมีการเปลี่ยนแปลง จะบันทึกการเปลี่ยนแปลงสถานะลงในคอนโซล
  • เพิ่ม การบันทึก Listener เหตุการณ์สำหรับเหตุการณ์ในวงจรที่จำเป็นทั้งหมด ซึ่งสุดท้ายแล้วจะทำให้การเรียก logStateChange() ผ่านในสถานะถัดไป

สิ่งที่ควรทราบเกี่ยวกับโค้ดนี้คือระบบจะเพิ่ม Listener เหตุการณ์ทั้งหมดลงใน window และ Listener ทั้งหมดจะส่งผ่าน {capture: true} ซึ่งอาจเป็นเพราะสาเหตุต่อไปนี้

  • เหตุการณ์วงจรชีวิตของหน้าเว็บบางรายการอาจมีเป้าหมายเดียวกัน pagehide และ pageshow จะทํางานใน window, visibilitychange, freeze และ resume จะทํางานใน document และ focus และ blur จะทํางานในองค์ประกอบ DOM ที่เกี่ยวข้อง
  • เหตุการณ์เหล่านี้ส่วนใหญ่จะไม่ทวีขึ้น ซึ่งหมายความว่าคุณจะเพิ่ม Listener เหตุการณ์แบบไม่จับไปยังองค์ประกอบบรรพบุรุษทั่วไปและสังเกตเหตุการณ์ทั้งหมดไม่ได้
  • ระยะการบันทึกจะทำงานก่อนระยะเป้าหมายหรือระยะบับเบิล ดังนั้นการเพิ่ม Listener ในส่วนนี้จะช่วยให้มั่นใจได้ว่า Listener จะทำงานก่อนที่โค้ดอื่นๆ จะยกเลิกได้

คําแนะนําสําหรับนักพัฒนาแอปในแต่ละสถานะ

ในฐานะนักพัฒนาซอฟต์แวร์ คุณควรทําความเข้าใจทั้งสถานะวงจรชีวิตของหน้าเว็บและวิธีสังเกตสถานะเหล่านั้นในโค้ด เนื่องจากประเภทงานที่ควร (และไม่ควร) ทํานั้นขึ้นอยู่กับสถานะของหน้าเว็บเป็นส่วนใหญ่

ตัวอย่างเช่น การแสดงการแจ้งเตือนชั่วคราวต่อผู้ใช้หากหน้าเว็บอยู่ในสถานะซ่อนอยู่นั้นไม่สมเหตุสมผล แม้ว่าตัวอย่างนี้ค่อนข้างชัดเจน แต่ก็มีคำแนะนำอื่นๆ ที่สังเกตได้ไม่ชัดเจนซึ่งควรค่าแก่การพิจารณา

รัฐ คําแนะนําสําหรับนักพัฒนาแอป
Active

สถานะใช้งานอยู่เป็นช่วงเวลาสําคัญที่สุดสําหรับผู้ใช้ และเป็นช่วงเวลาที่สําคัญที่สุดที่หน้าเว็บต้องตอบสนองต่ออินพุตของผู้ใช้

งานที่ไม่เกี่ยวข้องกับ UI ซึ่งอาจบล็อกชุดข้อความหลักควรลดลำดับความสำคัญเป็นช่วงเวลาที่ไม่มีการใช้งานหรือส่งไปยัง Web Worker

Passive

ในสถานะไม่โต้ตอบ ผู้ใช้ไม่ได้โต้ตอบกับหน้าเว็บ แต่ยังคงมองเห็นหน้าเว็บได้ ซึ่งหมายความว่าการอัปเดต UI และภาพเคลื่อนไหวจะยังคงราบรื่น แต่เวลาของการอัปเดตเหล่านี้จะสำคัญน้อยลง

เมื่อหน้าเว็บเปลี่ยนจาก active เป็น passive เป็นเวลาที่ดีที่จะคงสถานะของแอปพลิเคชันที่ไม่ได้บันทึกไว้

Hidden

เมื่อหน้าเว็บเปลี่ยนจากไม่ได้แสดงเป็นซ่อนอยู่ เป็นไปได้ว่าผู้ใช้จะไม่โต้ตอบกับหน้าเว็บนั้นอีกจนกว่าจะโหลดซ้ำ

การเปลี่ยนไปใช้แบบซ่อนมักเป็นการเปลี่ยนแปลงสถานะสุดท้ายที่นักพัฒนาแอปสังเกตได้อย่างน่าเชื่อถือ (โดยเฉพาะอย่างยิ่งในอุปกรณ์เคลื่อนที่เนื่องจากผู้ใช้ปิดแท็บหรือแอปเบราว์เซอร์ได้ และเหตุการณ์ beforeunload, pagehide และ unload จะไม่เริ่มทำงานในกรณีเหล่านั้น)

ซึ่งหมายความว่าคุณควรถือว่าสถานะซ่อนเป็นจุดที่มีโอกาสที่จะสิ้นสุดเซสชันของผู้ใช้ กล่าวคือ เก็บสถานะแอปพลิเคชันที่ไม่ได้บันทึกไว้และส่งข้อมูลวิเคราะห์ที่ไม่ได้ส่ง

นอกจากนี้ คุณควรหยุดการอัปเดต UI (เนื่องจากผู้ใช้จะไม่เห็น) และควรหยุดงานทั้งหมดที่ผู้ใช้ไม่ต้องการให้ทำงานอยู่เบื้องหลัง

Frozen

ในสถานะตรึงไว้ งานที่ตรึงได้ใน คิวงานจะถูกระงับจนกว่าหน้าเว็บจะเลิกตรึง ซึ่งอาจไม่เกิดขึ้นเลย (เช่น หากหน้าเว็บถูกยกเลิก)

ซึ่งหมายความว่าเมื่อหน้าเว็บเปลี่ยนจากซ่อนอยู่เป็นหยุดทำงาน คุณจะต้องหยุดตัวจับเวลาหรือยกเลิกการเชื่อมต่อทั้งหมด ซึ่งหากหยุดทำงานอาจส่งผลต่อแท็บอื่นๆ ที่เปิดอยู่ในต้นทางเดียวกัน หรือส่งผลต่อความสามารถของเบราว์เซอร์ในการวางหน้าเว็บในแคชย้อนกลับ/ไปข้างหน้า

โดยเฉพาะอย่างยิ่ง คุณต้องทำสิ่งต่อไปนี้

  • ปิดการเชื่อมต่อ IndexedDB ที่เปิดอยู่ทั้งหมด
  • ปิดการเชื่อมต่อ BroadcastChannel ที่เปิดอยู่
  • ปิดการเชื่อมต่อ WebRTC ที่ใช้งานอยู่
  • หยุดการสำรวจเครือข่ายหรือปิดการเชื่อมต่อ Web Socket ที่เปิดอยู่
  • ปล่อยการล็อกเว็บที่คงไว้ชั่วคราว

นอกจากนี้ คุณยังควรคงสถานะมุมมองแบบไดนามิก (เช่น ตำแหน่งเลื่อนในมุมมองรายการแบบไม่สิ้นสุด) ไปยัง sessionStorage (หรือ IndexedDB ผ่าน commit()) ที่ต้องการกู้คืนหากมีการทิ้งหน้าและโหลดซ้ำในภายหลัง

หากหน้าเว็บเปลี่ยนจากหยุดทำงานกลับไปเป็นซ่อนอยู่ คุณจะเปิดการเชื่อมต่อที่ปิดอยู่อีกครั้งหรือเริ่มการสำรวจอีกครั้งได้ ซึ่งคุณหยุดไว้เมื่อหน้าเว็บหยุดทำงานในตอนแรก

Terminated

โดยทั่วไปแล้ว คุณไม่จำเป็นต้องดำเนินการใดๆ เมื่อหน้าเว็บเปลี่ยนเป็นสถานะสิ้นสุด

เนื่องจากหน้าเว็บที่ยกเลิกการโหลดอันเป็นผลมาจากการกระทำของผู้ใช้จะผ่านสถานะซ่อนอยู่เสมอก่อนที่จะเข้าสู่สถานะสิ้นสุด สถานะซ่อนอยู่จึงเป็นสถานะที่ควรใช้ตรรกะการสิ้นสุดเซสชัน (เช่น สถานะแอปพลิเคชันที่เก็บไว้และการรายงานไปยัง Analytics)

นอกจากนี้ (ตามที่ระบุไว้ในคําแนะนําสําหรับสถานะซ่อนอยู่) นักพัฒนาแอปจําเป็นต้องทราบว่าในหลายกรณี (โดยเฉพาะในอุปกรณ์เคลื่อนที่) การตรวจหาการเปลี่ยนเป็นสถานะสิ้นสุดนั้นไม่น่าเชื่อถือ นักพัฒนาแอปที่พึ่งพาเหตุการณ์สิ้นสุด (เช่น beforeunload, pagehide และ unload) จึงมีแนวโน้มที่จะสูญเสียข้อมูล

Discarded

นักพัฒนาแอปจะไม่เห็นสถานะทิ้งในขณะที่ระบบทิ้งหน้าเว็บ ทั้งนี้เนื่องจากโดยทั่วไประบบจะทิ้งหน้าเว็บภายใต้ข้อจำกัดด้านทรัพยากร และการเลิกตรึงหน้าเพียงเพื่อให้สคริปต์ทำงานเพื่อตอบสนองต่อเหตุการณ์การยกเลิกนั้นไม่สามารถทำได้ในกรณีส่วนใหญ่

คุณจึงควรเตรียมพร้อมสำหรับกรณีที่ระบบจะทิ้งการเปลี่ยนแปลงจากซ่อนเป็นหยุดชั่วคราว จากนั้นคุณจะตอบสนองต่อการกู้คืนหน้าที่ทิ้งไปในเวลาที่หน้าเว็บโหลดได้โดยการตรวจสอบ document.wasDiscarded

เราขอย้ำอีกครั้งว่าความน่าเชื่อถือและลําดับของเหตุการณ์ในวงจรไม่ใช่สิ่งที่ติดตั้งใช้งานอย่างสอดคล้องกันในเบราว์เซอร์ทุกรุ่น วิธีง่ายที่สุดในการทําตามคําแนะนําในตารางคือการใช้ PageLifecycle.js

API วงจรของลูกค้าเดิมที่ควรหลีกเลี่ยง

คุณควรหลีกเลี่ยงเหตุการณ์ต่อไปนี้หากเป็นไปได้

เหตุการณ์การยกเลิกการโหลด

นักพัฒนาแอปจํานวนมากถือว่าเหตุการณ์ unload เป็นคอลแบ็กที่รับประกันและใช้เป็นสัญญาณสิ้นสุดเซสชันเพื่อบันทึกสถานะและส่งข้อมูลวิเคราะห์ แต่วิธีนี้ไม่น่าเชื่อถืออย่างยิ่ง โดยเฉพาะในอุปกรณ์เคลื่อนที่ เหตุการณ์ unload จะไม่ทริกเกอร์ในสถานการณ์การยกเลิกการโหลดทั่วไปหลายกรณี เช่น การปิดแท็บจากตัวสลับแท็บบนอุปกรณ์เคลื่อนที่ หรือการปิดแอปเบราว์เซอร์จากตัวสลับแอป

ด้วยเหตุนี้ คุณจึงควรใช้เหตุการณ์ visibilitychange เพื่อระบุเวลาที่เซสชันสิ้นสุดลง และพิจารณาสถานะซ่อนเป็นเวลาที่เชื่อถือได้ล่าสุดในการบันทึกข้อมูลแอปและผู้ใช้

นอกจากนี้ การมีตัวจัดการเหตุการณ์ unload ที่ลงทะเบียนไว้ (ผ่าน onunload หรือ addEventListener()) เพียงอย่างเดียวอาจทำให้เบราว์เซอร์ไม่สามารถจัดเก็บหน้าเว็บไว้ใน แคชย้อนกลับ/ไปข้างหน้า เพื่อให้โหลดหน้าเว็บย้อนกลับและไปข้างหน้าได้เร็วขึ้น

ในเบราว์เซอร์รุ่นใหม่ทั้งหมด เราขอแนะนําให้ใช้เหตุการณ์ pagehide เสมอเพื่อตรวจหากรณีที่หน้าเว็บอาจยกเลิกการโหลด (หรือที่เรียกว่าสถานะสิ้นสุด) แทนเหตุการณ์ unload หากจำเป็นต้องรองรับ Internet Explorer เวอร์ชัน 10 หรือต่ำกว่า คุณควรตรวจหาเหตุการณ์ pagehide และใช้ unload เฉพาะในกรณีที่เบราว์เซอร์ไม่รองรับ pagehide ดังนี้

const terminationEvent = 'onpagehide' in self ? 'pagehide' : 'unload';

window.addEventListener(terminationEvent, (event) => {
  // Note: if the browser is able to cache the page, `event.persisted`
  // is `true`, and the state is frozen rather than terminated.
});

เหตุการณ์ beforeunload

เหตุการณ์ beforeunload มีปัญหาคล้ายกับเหตุการณ์ unload ตรงที่ที่ผ่านมา การมีเหตุการณ์ beforeunload อาจทําให้หน้าเว็บไม่มีสิทธิ์ใช้ Back-Forward Cache เบราว์เซอร์สมัยใหม่ไม่มีข้อจำกัดนี้ แม้ว่าบางเบราว์เซอร์มีไว้เพื่อป้องกันล่วงหน้า เหตุการณ์ beforeunload จะไม่เริ่มทํางานเมื่อพยายามใส่หน้าเว็บลงใน Back-Forward Cache ซึ่งหมายความว่าเหตุการณ์ไม่น่าเชื่อถือเป็นสัญญาณเมื่อสิ้นสุดเซสชัน นอกจากนี้ เบราว์เซอร์บางประเภท (รวมถึง Chrome) กําหนดให้ต้องมีการโต้ตอบของผู้ใช้ในหน้าเว็บก่อนจึงจะอนุญาตให้เหตุการณ์ beforeunload เริ่มทํางานได้ ซึ่งจะส่งผลต่อความน่าเชื่อถือยิ่งขึ้น

ความแตกต่างอย่างหนึ่งระหว่าง beforeunload กับ unload คือ beforeunload มีการใช้งานที่ถูกต้อง เช่น เมื่อต้องการเตือนผู้ใช้ว่ามีการเปลี่ยนแปลงที่ไม่ได้บันทึก ผู้ใช้จะสูญหายหากยกเลิกการโหลดหน้าต่อไป

เนื่องจากมีเหตุผลอันสมควรในการใช้ beforeunload เราจึงขอแนะนำให้คุณเพิ่ม Listener beforeunload เท่านั้นเมื่อผู้ใช้มีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก แล้วนำการเปลี่ยนแปลงออกทันทีหลังจากที่มีการบันทึก

กล่าวคือ อย่าทำเช่นนี้ (เนื่องจากจะเพิ่ม beforeunload listener แบบไม่มีเงื่อนไข)

addEventListener('beforeunload', (event) => {
  // A function that returns `true` if the page has unsaved changes.
  if (pageHasUnsavedChanges()) {
    event.preventDefault();

    // Legacy support for older browsers.
    return (event.returnValue = true);
  }
});

ให้ทำแบบนี้แทน (เนื่องจากจะเพิ่ม Listener beforeunload เมื่อจำเป็นเท่านั้น และนำออกเมื่อไม่จำเป็นเท่านั้น)

const beforeUnloadListener = (event) => {
  event.preventDefault();
  
  // Legacy support for older browsers.
  return (event.returnValue = true);
};

// A function that invokes a callback when the page has unsaved changes.
onPageHasUnsavedChanges(() => {
  addEventListener('beforeunload', beforeUnloadListener);
});

// A function that invokes a callback when the page's unsaved changes are resolved.
onAllChangesSaved(() => {
  removeEventListener('beforeunload', beforeUnloadListener);
});

คำถามที่พบบ่อย

ทำไมจึงไม่มีสถานะ "กำลังโหลด"

Page Lifecycle API กําหนดสถานะให้แยกกันอยู่และไม่สามารถเกิดขึ้นพร้อมกันได้ เนื่องจากหน้าเว็บจะโหลดได้ในสถานะใช้งานอยู่ ไม่ได้ใช้งาน หรือซ่อนอยู่ และเนื่องจากหน้าเว็บสามารถเปลี่ยนสถานะหรือหยุดทำงานก่อนที่จะโหลดเสร็จสมบูรณ์ สถานะการโหลดแยกต่างหากจึงไม่มีความหมายในแพราไดม์นี้

หน้าเว็บของฉันทำงานสำคัญเมื่อถูกซ่อนอยู่ ฉันจะป้องกันไม่ให้หน้าเว็บถูกตรึงหรือยกเลิกได้อย่างไร

มีเหตุผลที่ถูกต้องมากมายที่หน้าเว็บไม่ควรค้างขณะทำงานในสถานะซ่อนอยู่ ตัวอย่างที่ชัดเจนที่สุดคือแอปที่เล่นเพลง

นอกจากนี้ ยังมีกรณีที่ Chrome ไม่ควรทิ้งหน้าเว็บ เช่น หากหน้าเว็บมีแบบฟอร์มที่มีข้อมูลที่ผู้ใช้ป้อนไว้แต่ยังไม่ได้ส่ง หรือมีbeforeunloadแฮนเดิลที่เตือนเมื่อมีการยกเลิกการโหลดหน้าเว็บ

ขณะนี้ Chrome จะระมัดระวังในการทิ้งหน้าเว็บ และจะทิ้งก็ต่อเมื่อมั่นใจว่าจะไม่ส่งผลกระทบต่อผู้ใช้ ตัวอย่างเช่น ระบบจะไม่ทิ้งหน้าเว็บที่ตรวจพบว่าทําสิ่งต่อไปนี้ขณะอยู่ในสถานะซ่อน เว้นแต่จะมีข้อจํากัดด้านทรัพยากรอย่างรุนแรง

  • กำลังเล่นเสียง
  • การใช้ WebRTC
  • การอัปเดตชื่อตารางหรือไอคอน Fav
  • การแสดงการแจ้งเตือน
  • การส่งข้อความ Push

สำหรับฟีเจอร์รายการปัจจุบันที่ใช้พิจารณาว่าสามารถตรึงแท็บหรือทิ้งแท็บได้อย่างปลอดภัยหรือไม่ โปรดดูที่วิธีที่ระบบวิเคราะห์การตรึงและทิ้งแท็บใน Chrome

แคชย้อนหลังคืออะไร

แคชย้อนกลับ/ไปข้างหน้าเป็นคำที่ใช้อธิบายการเพิ่มประสิทธิภาพการไปยังส่วนต่างๆ ที่บางเบราว์เซอร์นำมาใช้ ซึ่งทำให้การใช้ปุ่มย้อนกลับและไปข้างหน้าเร็วขึ้น

เมื่อผู้ใช้ไปยังหน้าอื่น เบราว์เซอร์เหล่านี้จะหยุดหน้าเว็บเวอร์ชันนั้นไว้ชั่วคราวเพื่อให้กลับมาใช้งานได้อย่างรวดเร็วในกรณีที่ผู้ใช้ไปยังหน้าก่อนหน้าโดยใช้ปุ่มย้อนกลับหรือไปข้างหน้า โปรดทราบว่าการเพิ่มเครื่องจัดการเหตุการณ์ unload ทำให้ไม่สามารถเพิ่มประสิทธิภาพนี้ได้

ในแง่ความตั้งใจและวัตถุประสงค์ทั้งหมด การหยุดทำงานนี้ทำงานเหมือนกับการหยุดทำงานของเบราว์เซอร์เพื่อประหยัด CPU/แบตเตอรี่ ด้วยเหตุนี้จึงถือว่าเป็นส่วนหนึ่งของสถานะวงจร frozen

หากเรียกใช้ API แบบไม่สอดคล้องกันในสถานะหยุดทำงานหรือหยุดทำงานไปแล้ว ฉันจะบันทึกข้อมูลไปยัง IndexedDB ได้อย่างไร

ในสถานะหยุดทำงานและสิ้นสุดการทำงาน ระบบจะระงับงานที่หยุดทำงานได้ในคิวงานของหน้า ซึ่งหมายความว่าคุณจะใช้ API แบบแอซิงโครนัสและแบบใช้การเรียกกลับ เช่น IndexedDB ได้อย่างไม่เสถียร

ในอนาคต เราจะเพิ่มเมธอด commit() ลงในออบเจ็กต์ IDBTransaction ซึ่งจะช่วยให้นักพัฒนาแอปสามารถดำเนินการในธุรกรรมแบบเขียนเท่านั้นได้อย่างมีประสิทธิภาพซึ่งไม่จำเป็นต้องมี Callback กล่าวคือ หากนักพัฒนาซอฟต์แวร์เพียงแค่เขียนข้อมูลไปยัง IndexedDB และไม่ดำเนินการธุรกรรมที่ซับซ้อนซึ่งประกอบด้วยการอ่านและเขียน วิธี commit() จะเสร็จสิ้นได้ก่อนที่คิวงานจะถูกระงับ (สมมติว่าฐานข้อมูล IndexedDB เปิดอยู่)

อย่างไรก็ตาม สำหรับโค้ดที่ต้องใช้ในปัจจุบัน นักพัฒนาซอฟต์แวร์มี 2 ตัวเลือก ได้แก่

  • ใช้พื้นที่เก็บข้อมูลของเซสชัน: พื้นที่เก็บข้อมูลของเซสชัน ทำงานแบบซิงค์และจะยังคงอยู่เมื่อมีการทิ้งหน้าเว็บ
  • ใช้ IndexedDB จาก Service Worker: โปรแกรมทำงานของบริการสามารถจัดเก็บข้อมูลใน IndexedDB หลังสิ้นสุดหรือยกเลิกหน้าแล้ว ใน freeze หรือ ฟังก์ชันการเรียกเหตุการณ์ pagehide คุณสามารถส่งข้อมูลไปยัง Service Worker ผ่าน postMessage() และ Service Worker จะจัดการการบันทึกข้อมูลได้

การทดสอบแอปในสถานะหยุดทำงานและถูกทิ้ง

หากต้องการทดสอบลักษณะการทำงานของแอปในสถานะหยุดทำงานและถูกทิ้ง ให้ไปที่ chrome://discards เพื่อหยุดทำงานหรือทิ้งแท็บที่เปิดอยู่

UI การทิ้งของ Chrome
UI ของ Chrome Discards

วิธีนี้ช่วยให้มั่นใจได้ว่าหน้าเว็บจะจัดการเหตุการณ์ freeze และ resume รวมถึง Flag document.wasDiscarded อย่างถูกต้องเมื่อมีการโหลดหน้าเว็บอีกครั้งหลังจากการทิ้ง

สรุป

นักพัฒนาแอปที่ต้องการเคารพทรัพยากรระบบของอุปกรณ์ของผู้ใช้ควรสร้างแอปโดยคำนึงถึงสถานะวงจรชีวิตของหน้าเว็บ หน้าเว็บไม่ควรใช้ทรัพยากรระบบมากเกินไปในสถานการณ์ที่ผู้ใช้ไม่คาดคิด

ยิ่งนักพัฒนาซอฟต์แวร์เริ่มใช้ Page Lifecycle API ใหม่มากเท่าใด เบราว์เซอร์ก็จะยิ่งปลอดภัยและตรึงหน้าเว็บที่ไม่ได้ใช้งานมากขึ้นเท่านั้น ซึ่งหมายความว่าเบราว์เซอร์จะใช้หน่วยความจำ, CPU, แบตเตอรี่ และทรัพยากรเครือข่ายน้อยลง ซึ่งถือเป็นเรื่องดีสำหรับผู้ใช้