EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Web / รูปไม่โหลดในหน้าที่ซ่อนด้วย display:none เฉพาะบนมือถือ
Web

รูปไม่โหลดในหน้าที่ซ่อนด้วย display:none เฉพาะบนมือถือ

โดย Ton-Or ·17 ต.ค. 2026·อ่าน 9 นาที
รูปไม่โหลดในหน้าที่ซ่อนด้วย display:none เฉพาะบนมือถือ

บั๊กที่เสียเวลาที่สุดไม่ใช่บั๊กที่หาสาเหตุไม่เจอ แต่เป็นบั๊กที่เครื่องมือตรวจทุกตัวบอกว่าปกติ ไฟล์คืน 200 เปิดลิงก์ตรงก็เห็นรูป เรียก fetch ก็ได้ เรียก new Image ก็ได้ แต่บนจอมือถือมันดำอยู่อย่างนั้น เพราะปัญหาไม่ได้อยู่ที่ไฟล์ มันอยู่ที่จังหวะการมองเห็น

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

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

โครงของหน้าที่ทำให้เกิดอาการ

โครงนี้ธรรมดามาก ใครทำเว็บแบบ one-page ที่มีหลายหน้าย่อยน่าจะเขียนคล้ายกัน คือเอาทุกหน้าใส่ไว้ใน HTML ตั้งแต่แรก แล้วซ่อนทั้งหมดไว้ เปิดเฉพาะหน้าที่ active

<!-- index.html — ทุกหน้าอยู่ใน DOM ตั้งแต่แรก ซ่อนด้วย CSS -->
<main>
  <section id="p-home"    class="page on">...</section>
  <section id="p-rooms"   class="page">
    <div class="big"><img id="rmain" src="" alt="" width="1200" height="900" loading="lazy"></div>
    <div id="rthumbs"></div>
  </section>
  <section id="p-gallery" class="page">
    <img src="/img/gallery/g-01.jpg" alt="..." width="1200" height="800" loading="lazy">
    <img src="/img/gallery/g-02.jpg" alt="..." width="1200" height="800" loading="lazy">
  </section>
</main>
/* style.css — หน้าไหนไม่ active คือหายไปจากผังทั้งก้อน */
.page      { display: none; }
.page.on   { display: block; }

ส่วนตัวสลับหน้าก็ตรงไปตรงมา ถอด class on ออกจากหน้าเดิม ใส่เข้าหน้าใหม่ จบ

// app.js — เวอร์ชันแรก สลับหน้าอย่างเดียว ไม่ยุ่งกับรูป
const order = ['home', 'rooms', 'gallery', 'map', 'reservation', 'contact'];
let cur = 'home';

function go(id) {
  if (!order.includes(id) || id === cur) return;
  document.getElementById('p-' + cur).classList.remove('on');
  document.getElementById('p-' + id).classList.add('on');
  cur = id;
  try { history.replaceState(null, '', '#' + id); } catch (_) {}
  window.scrollTo({ top: 0, behavior: 'smooth' });
}

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

สองทางที่พาหลงทาง: fetch ที่สำเร็จ และ curl ที่อ่านผิด

สิ่งแรกที่ผมทำคือสงสัยไฟล์ก่อน เปิด DevTools บนมือถือผ่าน remote debugging แล้วยิงทดสอบใน console ตรง ๆ

// ทดสอบใน console ของเครื่องที่มีอาการ — ทั้งสองทางผ่านหมด
const u = '/img/gallery/g-01.jpg';

fetch(u).then(r => console.log('[fetch]', r.status, r.headers.get('content-type')));
// [fetch] 200 image/jpeg

const t = new Image();
t.onload  = () => console.log('[new Image] ok', t.naturalWidth, 'x', t.naturalHeight);
t.onerror = () => console.log('[new Image] FAIL');
t.src = u;
// [new Image] ok 1200 x 800

ทั้ง fetch และ new Image สำเร็จทั้งคู่ ได้ 200 ได้ content-type ถูก ได้ขนาดจริง แปลว่าไฟล์ไม่เสีย เส้นทางเน็ตไม่มีปัญหา สิทธิ์ไม่มีปัญหา และเบราว์เซอร์ตัวนั้นโหลดรูปไฟล์นั้นได้ ตอนนั้นผมเกือบสรุปว่าเป็นเรื่อง server แล้วครับ เพราะหลักฐานมันชี้ไปทางนั้นหมด

การทดสอบที่แยกออกมาจากบริบท จะตอบคำถามผิดข้อ

ที่ผมทำผิดคือไปทดสอบว่า ไฟล์โหลดได้ไหม ทั้งที่คำถามจริงคือ ทำไม element ตัวนั้นถึงไม่สั่งโหลด การเรียก fetch หรือ new Image คือการสร้าง request ขึ้นมาใหม่นอกผังหน้าเว็บ มันเลยเลี่ยงกลไกทั้งหมดที่เป็นต้นเหตุไปโดยปริยาย ผลที่ได้เป็นจริงแต่ไม่เกี่ยวกับอาการเลยสักนิด

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

$ curl -I https://example.com/img/gallery/g-01.jpg
HTTP/1.1 200 OK
content-type: image/jpeg
content-length: 184213
cache-control: public, max-age=2592000

เห็น HTTP/1.1 ปุ๊บผมก็คิดทันทีว่าเจอแล้ว เพราะ HTTP/1.1 มีเพดานจำนวน connection ต่อ origin ประมาณ 6 ช่อง หน้าแกลเลอรีมีรูป 26 รูป ถ้าคิวมันตันก็อธิบายได้พอดีว่าทำไมบางรูปโผล่ช้าบางรูปไม่โผล่ ผมเลยไปนั่งวางแผนย้ายรูปไปโดเมนย่อยเพื่อกระจาย connection เรียบร้อย ก่อนจะมาเจอว่าตัวเองอ่านผลผิด

# curl ไม่ต่อรอง h2 ให้เองถ้าไม่สั่ง — ต้องบอกมันตรง ๆ
$ curl -I --http2 https://example.com/img/gallery/g-01.jpg
HTTP/2 200
content-type: image/jpeg

# ของจริงดูที่เบราว์เซอร์เท่านั้น: DevTools > Network > คลิกขวาที่หัวตาราง > ติ๊ก Protocol
# ทุก request ของหน้านี้ขึ้น h2 และไม่มี request ไหนค้างในสถานะ pending เลยสักตัว

ปลายทางเป็น HTTP/2 มาตั้งแต่ต้น ที่เห็นเป็น HTTP/1.1 เพราะ curl ไม่ต่อรอง h2 ให้เองถ้าไม่ใส่ --http2 และที่สำคัญกว่านั้นคือถ้าเป็นเรื่องคิว connection จริง ผมต้องเห็น request ค้างอยู่ในสถานะ pending บ้าง แต่ในแท็บ Network ของเครื่องที่มีอาการ ไม่มี request ของรูปเหล่านั้นโผล่มาเลยแม้แต่ตัวเดียว ไม่ใช่ค้าง ไม่ใช่ error คือไม่ถูกยิงออกไปตั้งแต่แรก

ศูนย์ request ไม่ใช่ปัญหาเครือข่าย

บรรทัดนี้คือจุดพลิกของงานทั้งงาน ถ้า request ค้างหรือ error นั่นเป็นเรื่องเครือข่าย แต่ถ้าจำนวน request ของรูปที่หายไปเป็น 0 แปลว่าเบราว์เซอร์ไม่เคยตัดสินใจจะโหลดมันเลย ปัญหาอยู่ฝั่ง render ล้วน ๆ ต่อให้ไปแก้ server หรือย้าย CDN อีกสิบรอบก็ไม่มีทางหาย

ตัวจริง: lazy-load ไม่ยิงตอนหน้าเปลี่ยนจาก none เป็น block

พอรู้ว่าเป็นเรื่อง render ผมกลับไปดูสิ่งที่เขียนไว้เองในแท็ก คือ loading="lazy" ซึ่งเป็นของดีที่ผมใส่ให้ทุกรูปโดยอัตโนมัติจนเคยชิน กลไกของมันคือเบราว์เซอร์จะเฝ้าดูว่า element ตัวนั้นเข้าใกล้ viewport หรือยัง ถ้ายังก็ไม่ยิง request เบื้องหลังคือ IntersectionObserver

ทีนี้ element ที่อยู่ในซับทรีที่ display: none มันไม่มีกล่องในผังหน้าเว็บ ไม่มีตำแหน่ง ไม่มีขนาด ตัวเฝ้าดูเลยตอบว่าไม่อยู่ใน viewport ซึ่งถูกต้องตามหลัก พอผมถอด display: none ออก element กลับเข้าผัง แต่การกลับเข้าผังของ element ที่เคยถูกตัดสินไปแล้วว่า "ยังไม่ต้องโหลด" ไม่ได้ทำให้เบราว์เซอร์รีบทบทวนคำตัดสินนั้นใหม่ทันที มันไปรอ relayout รอบถัดไป ซึ่งบางทีมาช้ามาก

// พิสูจน์ว่ารูปยังไม่ถูกโหลด: naturalWidth เป็น 0 คือยังไม่มีข้อมูลภาพ
function auditImages(pageId) {
  const rows = [...document.querySelectorAll(`#p-${pageId} img`)].map(im => ({
    src: im.getAttribute('src')?.slice(-24),
    loading: im.loading,          // lazy | eager
    complete: im.complete,        // true ทั้งที่ยังไม่มีภาพได้ ถ้า src ว่าง
    natural: im.naturalWidth,     // 0 = ยังไม่โหลดจริง ตัวนี้เชื่อได้
  }));
  console.table(rows);
}
auditImages('gallery');
// ผลบนมือถือหลังกดเข้าหน้า gallery: natural = 0 ทั้ง 26 แถว
// รออีก 5 วินาทีแล้วเรียกซ้ำ: บางแถวกลายเป็น 1200 เอง โดยไม่ได้ทำอะไรเพิ่ม

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

ลำดับที่ทำให้รูปหายimg อยู่ในหน้าที่ซ่อนdisplay none->ไม่มีกล่องในผังไม่อยู่ใน viewport->lazy ตัดสินว่ายังไม่โหลดrequest = 0->กดเปิดหน้าnone เป็น block->รอ relayoutช้าถึง 5 วินาที

ทำไมถึงออกเฉพาะบนมือถือ

คำถามนี้สำคัญ เพราะถ้าไม่ตอบให้ได้ก็จะไม่มั่นใจว่าแก้ถูกจุด เดสก์ท็อปกับมือถือต่างกันสองเรื่อง

เรื่องแรกคือขนาด viewport เดสก์ท็อปจอสูง 900 พิกเซลขึ้นไป พอเปิดหน้าใหม่ รูปแถวแรกหลายตัวตกอยู่ในระยะที่ถือว่า "ใกล้จะเห็นแล้ว" อยู่ดี เบราว์เซอร์เลยยิง request ให้ทันที ส่วนมือถือจอสูงราว 600 พิกเซลและรูปวางเรียงเป็นคอลัมน์เดียว รูปที่สองลงไปเลยอยู่นอกระยะทั้งหมด กลายเป็นว่ามือถือมีจำนวนรูปที่ตกอยู่ในสถานะ lazy มากกว่าเยอะ

เรื่องที่สองคือระยะ prefetch ของ lazy-load ไม่คงที่ Chromium ปรับระยะตามคุณภาพเน็ตที่มันเดาได้ ถ้าเข้าใจว่าเป็น 4G หรือช้ากว่านั้น ระยะจะสั้นลงเพื่อประหยัดดาต้า เครื่องที่ต่อไวไฟแรงกับเครื่องที่ต่อเน็ตมือถือเลยให้ผลไม่เหมือนกัน อาการแบบนี้ทำซ้ำยากถ้าเทสต์แต่บนโหมดจำลองมือถือใน DevTools ที่ยังใช้เน็ตของเครื่องเดสก์ท็อป

เดสก์ท็อป

บังเอิญรอด ไม่ใช่ถูกต้อง

  • viewport สูง รูปแถวแรกเข้าระยะเอง
  • เน็ตเร็ว ระยะ prefetch กว้างกว่า
  • เมาส์ขยับบ่อย relayout มาถี่
  • ผลคือเห็นรูปทันที ทั้งที่กลไกเดียวกัน
มือถือ

เจอเต็ม ๆ ทุกเงื่อนไข

  • viewport เตี้ย รูปตกนอกระยะเกือบหมด
  • เน็ตช้ากว่า ระยะ prefetch แคบลง
  • นิ่งอยู่กับที่ ไม่มีอะไรกระตุ้น relayout
  • ผลคือกรอบเปล่า แล้วค่อยโผล่ทีหลัง

ตั้ง loading เป็น eager เฉย ๆ ไม่พอ

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

// เวอร์ชันที่ผมลองก่อน แล้วไม่ได้ผล
document.querySelectorAll('#p-gallery img[loading="lazy"]')
  .forEach(im => { im.loading = 'eager'; });
// naturalWidth ยังเป็น 0 เหมือนเดิม บาง build ได้บ้างไม่ได้บ้าง เชื่อถือไม่ได้

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

// บังคับให้เริ่มกระบวนการโหลดใหม่ — ถอด src ออกก่อน แล้วค่อยใส่ค่าเดิมกลับ
function forceLoad(root) {
  root.querySelectorAll('img').forEach(im => {
    if (im.naturalWidth) return;                 // โหลดไปแล้ว อย่าไปยุ่ง
    const s = im.getAttribute('src');
    if (!s) return;                              // รูปที่ JS จะเซ็ต src ทีหลัง ข้ามไป
    im.loading = 'eager';
    im.removeAttribute('src');                   // สองบรรทัดนี้ต้องคู่กัน
    im.src = s;                                  // การเซ็ตค่าใหม่คือตัวสั่งให้เริ่มโหลด
  });
}
ทำไมต้องถอดก่อนแล้วค่อยใส่ค่าเดิม

ถ้าเซ็ต im.src = im.src เฉย ๆ ค่าไม่เปลี่ยน บาง engine จะไม่ถือว่าเป็นการเปลี่ยนแปลงและไม่เริ่มโหลดใหม่ การ removeAttribute แล้วค่อยใส่กลับ ทำให้ค่าเปลี่ยนจริงสองครั้ง เลยได้ผลแน่นอนกว่า และอย่าลืมเช็ก naturalWidth ก่อนเสมอ ไม่งั้นรูปที่โหลดครบแล้วจะกะพริบทุกครั้งที่ผู้ใช้สลับหน้าไปมา

ทางแก้จริงต้องทำสองอย่างคู่กัน

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

// อุ่นแคชหลังหน้าแรกนิ่งแล้ว 1 วินาที — ทีละ 4 ตัว ไม่แย่งแบนด์วิดท์กับหน้าที่ผู้ใช้กำลังดู
setTimeout(() => {
  const urls = [];
  ROOMS.forEach(r => { for (let k = 1; k <= r.n; k++) urls.push(`/img/rooms/${r.id}/${r.id}-${k}.jpg`); });
  GALLERY.forEach(g => urls.push(g.src));

  let i = 0;
  const next = () => {
    if (i >= urls.length) return;
    const im = new Image();
    im.onload = im.onerror = next;      // ต่อคิวทั้งสำเร็จและพัง อย่าให้ค้างเพราะรูปเดียว
    im.src = urls[i++];
  };
  for (let c = 0; c < 4; c++) next();   // 4 เส้นพร้อมกันกำลังดี
}, 1000);

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

หลังกดเข้าหน้าแกลเลอรี 26 รูปบนมือถือ0 s1 s2 s3 s4 s5 sก่อนแก้request = 0 รูปว่างทั้ง 26 รูปหลังแก้ครบทุกใบforceLoad สั่งโหลด เจอของในแคชที่อุ่นไว้กดเปิดหน้าrelayout ราว 5 วิ
แกนเวลาเริ่มที่ตอนกดเปิดหน้า ก่อนแก้รอ relayout ส่วนหลังแก้รูปขึ้นครบใน 1.5 วินาที
// app.js — เวอร์ชันที่ใช้จริง
function go(id) {
  if (!order.includes(id) || id === cur) return;
  const c = document.getElementById('p-' + cur);
  const n = document.getElementById('p-' + id);
  if (c) c.classList.remove('on');
  if (n) n.classList.add('on');
  cur = id;

  if (id === 'rooms') renderRoom(curRoom);   // หน้านี้ src มาจาก JS ต้อง render ก่อน
  if (n) forceLoad(n);                       // แล้วค่อยบังคับโหลดทั้งหน้า

  try { history.replaceState(null, '', '#' + id); } catch (_) {}
  window.scrollTo({ top: 0, behavior: 'smooth' });
}

รูปที่ src ถูกเซ็ตด้วย JS ต้องจัดการต่างหาก

มีรูปประเภทหนึ่งที่ forceLoad ช่วยไม่ได้ คือรูปหลักที่ผมใช้ element เดิมตัวเดียวแล้วเปลี่ยน src ตามห้องที่ผู้ใช้เลือก ตอนหน้ายังซ่อนอยู่ค่า src เป็นสตริงว่าง forceLoad เลยข้ามไปตามที่เขียนไว้ ต้องให้ตัว render เป็นคนจัดการเอง

// render รูปห้อง — element เดิม เปลี่ยน src ตามห้องที่เลือก
function renderRoom(i) {
  const r = ROOMS[i];
  const main = document.getElementById('rmain');

  main.loading = 'eager';                  // ตัวนี้ผู้ใช้ต้องเห็นแน่ ๆ ไม่ต้อง lazy
  main.src = `/img/rooms/${r.id}/${r.id}-1.jpg`;   // เซ็ตสดทุกครั้ง ไม่เช็กว่าซ้ำเดิมไหม
  main.alt = r.alt;

  document.getElementById('rthumbs').innerHTML = r.thumbs
    .map((t, k) => `<button class="${k ? '' : 'on'}" data-full="${t}">
         <img src="${t}" alt="${r.alt} รูปที่ ${k + 1}" loading="eager"></button>`)
    .join('');
}

ตรงนี้ผมเลิกใช้ loading="lazy" กับ thumbnail ในหน้านี้ไปเลยครับ เพราะมันเป็นรูปเล็กจำนวนไม่เกินสิบใบ และเป็นรูปที่ผู้ใช้ตั้งใจกดเข้ามาดู การประหยัด request สิบตัวแลกกับหน้าที่ดูเหมือนพัง ไม่คุ้มกันเลย

ตัวตรวจอัตโนมัติก่อนบอกว่าเสร็จ

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

// tools/check-images.cjs — ต้องได้ 0 ทุกหน้าก่อนถึงจะส่งงาน
const { chromium } = require('playwright');
const PAGES = ['home', 'rooms', 'gallery', 'map', 'reservation', 'contact'];

(async () => {
  const b = await chromium.launch();
  const ctx = await b.newContext({ viewport: { width: 390, height: 640 }, isMobile: true });
  const p = await ctx.newPage();
  const errs = [];
  p.on('pageerror', e => errs.push('PAGEERROR ' + e.message));

  await p.goto('https://example.com/?v=' + Date.now(), { waitUntil: 'load' });
  await p.waitForTimeout(2500);            // เผื่อเวลาให้ตัวอุ่นแคชทำงาน

  let bad = 0;
  for (const id of PAGES) {
    await p.evaluate(x => window.go(x), id);
    await p.waitForTimeout(1500);          // ให้เวลาดาวน์โหลดจริง
    const blank = await p.evaluate(x =>
      [...document.querySelectorAll(`#p-${x} img`)]
        .filter(im => im.naturalWidth === 0)
        .map(im => im.getAttribute('src') || '(src ว่าง)'), id);
    console.log(`[${blank.length ? 'FAIL' : 'OK'}] ${id} — รูปที่ยังไม่โหลด ${blank.length}`);
    blank.forEach(s => console.log('      ', s));
    bad += blank.length;
  }
  if (errs.length) console.log(errs.join('\n'));
  await b.close();
  process.exit(bad || errs.length ? 1 : 0);
})();

อีกเรื่องที่เกี่ยวข้องกันตอนนั่งแก้วนไปวนมาคือ HTML ที่ถูกแคชไว้ ผมโดนหลอกอยู่พักหนึ่งว่าแก้แล้วไม่เปลี่ยน ทั้งที่ไฟล์ขึ้นไปแล้ว ระหว่างช่วงที่ยังแก้ถี่ ๆ ผมสั่งไม่ให้แคช HTML ไว้ก่อน

# .htaccess — ระหว่างช่วงแก้งานถี่ ๆ ให้ HTML หมดอายุทันที รูปกับ CSS ยังแคชได้ตามเดิม
<IfModule mod_expires.c>
  ExpiresActive On
  ExpiresByType text/html  "access plus 0 seconds"
  ExpiresByType image/jpeg "access plus 30 days"
  ExpiresByType text/css   "access plus 7 days"
</IfModule>
<IfModule mod_headers.c>
  <FilesMatch "\.(html|php)$">
    Header set Cache-Control "no-cache, must-revalidate"
  </FilesMatch>
</IfModule>

ตัวเลขก่อนกับหลัง และลำดับที่ใช้กับงานหน้า

26 รูปจำนวนรูปในหน้าแกลเลอรี
0request ที่ถูกยิงตอนเปิดหน้า
5 วิเวลาที่รูปโผล่เองแบบสุ่ม
1.5 วิเวลาหลังแก้ ครบทุกใบ
4 เส้นจำนวนคิวของตัวอุ่นแคช
1000 msหน่วงก่อนเริ่มอุ่นแคช
390 pxความกว้างจอที่ใช้ตรวจ
6 หน้าหน้าที่ตัวตรวจไล่ทุกรอบ
ลำดับหลังแก้โหลดหน้าแรก->อุ่นแคชเบื้องหลังหน่วง 1 วินาที->กดเปิดหน้าrender ก่อนถ้ามี->forceLoadถอด src ใส่กลับ->เจอของในแคชขึ้นครบ 1.5 วินาที

ลำดับที่ผมใช้ตรวจทุกครั้งเวลาเจออาการรูปไม่ขึ้น สรุปเป็นขั้นตอนได้แบบนี้

  1. อาการรูปไม่ขึ้น ให้ดู จำนวน request ในแท็บ Network ก่อนเสมอ ถ้าเป็นศูนย์คือเรื่อง render ไม่ใช่เรื่อง server
  2. เลิกใช้ fetch หรือ new Image เป็นหลักฐานว่าไฟล์ปกติ มันตอบคนละคำถามกับอาการ
  3. อ่านผล curl ให้ครบ ถ้าจะดูโปรโตคอลจริงต้องใส่ --http2 หรือดูคอลัมน์ Protocol ใน DevTools
  4. เช็ก naturalWidth ของทุก img ในหน้าที่เพิ่งเปิด ค่า 0 คือยังไม่โหลดจริง อย่าเชื่อ complete
  5. แก้ด้วยสองอย่างคู่กัน อุ่นแคชล่วงหน้า และบังคับ removeAttribute('src') แล้วใส่ค่าเดิมกลับตอนเปิดหน้า
  6. รูปที่ src มาจาก JS ต้องให้ตัว render เซ็ตสดทุกครั้ง อย่าไปหวังพึ่งลูปบังคับโหลด
  7. ผูกตัวตรวจด้วยเบราว์เซอร์จริงขนาดจอมือถือไว้ท้ายงาน ให้มันตอบว่าศูนย์ก่อนถึงจะบอกว่าเสร็จ

ส่วนตัวผมมองว่ารากของเรื่องนี้คือการเอา display: none มาใช้กับหน้าย่อยทั้งหน้า ซึ่งมันคือการบอกเบราว์เซอร์ว่า "ของพวกนี้ไม่มีอยู่จริง" แล้วเราก็ไปคาดหวังให้มันเตรียมของพวกนี้ไว้ให้ ถ้าเขียนใหม่ตั้งแต่ต้นและรับความซับซ้อนได้ ผมจะเลือกทางที่ไม่ต้องสู้กับเบราว์เซอร์แบบนี้ คือแยกเป็นหน้าจริงคนละไฟล์ไปเลย หรือถอด DOM ของหน้าที่ไม่ใช้ออกแล้วสร้างใหม่ตอนเปิด แต่ถ้ามีเว็บแบบนี้อยู่ในมือแล้วและไม่คุ้มจะรื้อ สองอย่างที่ผมเขียนไว้ข้างบนแก้ได้จริงและไม่ต้องแตะโครงเดิม ใครจะเอาไปปรับจำนวนคิวหรือเวลาหน่วงให้เข้ากับงานตัวเองก็ตามสะดวกครับ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย