Vì sao đồng hồ trong trình duyệt bị trôi khi tab chạy nền

Độ chính xác của bấm giờ và đếm ngược trong trình duyệt: tab nền làm trễ bộ hẹn giờ ra sao, phép tính của một đồng hồ bị trôi, và cách làm để luôn đúng.

Trong bài này

Bật đếm ngược 25 phút trong một tab trình duyệt, chuyển sang tab khác, nửa tiếng sau quay lại, và nhiều trang hẹn giờ online vẫn còn hiện vài phút. Máy tính của bạn không hỏng gì cả: trình duyệt cố ý làm chậm những trang bạn không xem, và một đồng hồ được viết theo cách ngây thơ sẽ biến sự chậm lại đó thành thời gian bị mất. Bài này giải thích trình duyệt thực sự làm gì, tính ra các con số, và cho thấy vì sao đồng hồ bấm giờ dễ làm đúng còn chuông của đồng hồ đếm ngược thì khó hơn. Đồng hồ bấm giờ và đếm ngược và đồng hồ Pomodoro trên trang này được làm theo cách mô tả ở cuối bài.

Trình duyệt làm gì với tab bị ẩn

Bộ hẹn giờ JavaScript (setTimeout và setInterval) chưa bao giờ hứa sẽ chính xác. Tài liệu MDN liệt kê các lý do một bộ hẹn giờ chạy muộn hơn yêu cầu: khi lồng nhau đến cấp thứ năm, trình duyệt áp mức tối thiểu 4 mili giây; hàm gọi lại không thể chạy cho đến khi đoạn mã đang chạy kết thúc; và để tiết kiệm pin, trình duyệt áp một độ trễ tối thiểu cho tab không hoạt động. Chi tiết khác nhau giữa các trình duyệt. MDN đưa ra số liệu của Firefox: Firefox trên máy tính dùng mức tối thiểu một giây cho tab không hoạt động, còn Firefox trên Android là 15 phút và có thể gỡ hẳn tab đó khỏi bộ nhớ.

Chrome công bố quy tắc của mình khi phát hành phiên bản 88 vào tháng 1 năm 2021. Trong tab bị ẩn, bộ hẹn giờ thường chỉ được kiểm tra mỗi giây một lần. Thêm vào đó là "hãm mạnh" (intensive throttling), khi bộ hẹn giờ chỉ được kiểm tra mỗi phút một lần. Nó áp dụng khi tất cả các điều sau đều đúng: trang đã bị ẩn hơn năm phút, bộ hẹn giờ nằm trong một chuỗi từ năm lần trở lên (một setTimeout hẹn tiếp setTimeout kế tiếp, hoặc một setInterval, vì mỗi lần chạy đều được tính vào chuỗi), trang đã im lặng ít nhất 30 giây, và không dùng WebRTC. Một đồng hồ đang chạy trên trang web thỏa mãn đủ mọi điều kiện đó sau năm phút yên lặng ở nền.

requestAnimationFrame, cách thường dùng để vẽ hoạt ảnh mượt, còn đi xa hơn: nó chờ đến khi trang hiện lại, nên trong tab bị ẩn nó hoàn toàn không chạy.

Phép tính của một đồng hồ đếm ngược bị trôi

Đồng hồ đếm ngược ngây thơ giữ một số giây và trừ đi một mỗi khi bộ hẹn giờ mỗi giây được gọi. Nó giả định mọi nhịp cách nhau đúng một giây. Đây là cái giá của giả định đó.

Tab đang hiển thị, trễ nhỏ. Giả sử mỗi nhịp, được hẹn bằng setTimeout từ nhịp trước, chạy trễ 4 ms. Bộ đếm về 0 sau 1.500 nhịp, nhưng 1.500 nhịp dài 1,004 giây mất 1.506 giây: chuông 25 phút reo muộn 6 giây. Không nhiều, nhưng nó tăng theo độ dài của bộ đếm và độ bận của trang.

Tab bị ẩn, Chrome hãm mạnh. Bắt đầu cũng bộ đếm 25 phút đó và chuyển tab ngay. Trong năm phút đầu, các nhịp vẫn đến khoảng mỗi giây một lần, nên bộ đếm đã trừ khoảng 300 giây. Sau đó, bộ hẹn giờ trong chuỗi chạy khoảng mỗi phút một lần, và mỗi lần chỉ trừ một giây. Khi 25 phút thực đã trôi qua, bộ đếm mới trừ chừng 300 + 20 = 320 giây và còn hiện khoảng 19:40. Với tốc độ trừ một giây mỗi phút, 1.180 giây còn lại sẽ mất khoảng 20 giờ mới đếm hết. Con số chính xác tùy phiên bản trình duyệt và trang web, nhưng chiều hướng luôn như nhau: đếm nhịp sẽ mất thời gian mỗi khi nhịp đến muộn.

Bấm giờ và đếm ngược khác nhau thế nào

Cách sửa phần đếm giống nhau cho cả hai: đừng đếm nhịp, hãy lưu một mốc thời gian.

  • Đồng hồ bấm giờ lưu thời điểm bắt đầu. Thời gian đã trôi là bây giờ trừ lúc bắt đầu, cộng với phần của những lần chạy trước. Mỗi lần trang vẽ lại, nó đọc đồng hồ và tính ra kết quả, nên một nhịp đến muộn hay bị bỏ chỉ làm chậm phần hiển thị. Quay lại sau một giờ chạy nền, khung hình đầu tiên đã hiện đúng giờ. Không có gì phải xảy ra khi bạn vắng mặt, nên đồng hồ bấm giờ dễ làm đúng.
  • Đồng hồ đếm ngược lưu thời điểm sẽ kết thúc. Thời gian còn lại là lúc kết thúc trừ bây giờ, luôn đúng mỗi khi được tính. Nhưng đếm ngược còn một việc thứ hai: nó phải làm gì đó khi hết giờ, như reo chuông hay hiện thông báo, trong lúc bạn không nhìn. Việc này cần trang thức dậy đúng lúc, mà đó lại chính là thứ bị hãm.

Vì vậy riêng phần chuông của đồng hồ đếm ngược cần cẩn thận hơn. Có hai kỹ thuật giúp ích. Thứ nhất, hẹn một timeout duy nhất vào đúng thời điểm kết thúc thay vì trông vào nhịp hiển thị lặp lại. Một timeout đơn lẻ không nằm trong chuỗi, nên quy tắc mỗi phút một lần của Chrome không áp dụng cho nó; nó chỉ chịu mức kiểm tra thông thường mỗi giây một lần của tab bị ẩn. Thứ hai, hẹn trước âm thanh trên đồng hồ của Web Audio. Âm thanh được xử lý trên một luồng riêng, và một âm được hẹn phát tại một mốc thời gian âm thanh sẽ phát đúng lúc đó, kể cả khi bộ hẹn giờ JavaScript của trang bị hãm. Trình duyệt chỉ cho trang bắt đầu phát âm thanh sau một cú bấm, nên nút bắt đầu là lúc để chuẩn bị việc này.

Nên đọc đồng hồ nào

Date.now() trả về giờ thực của hệ thống; performance.now() trả về thời gian độ phân giải cao từ một đồng hồ đơn điệu. MDN chỉ ra sự đánh đổi: Date.now() có thể bị ảnh hưởng khi hệ thống hoặc người dùng chỉnh giờ, còn performance.now() không bao giờ giảm và không bị chỉnh. Với các phép đo ngắn như đo hiệu năng, performance.now() là lựa chọn tốt hơn. Với bộ hẹn giờ có thể chạy qua lúc laptop ngủ, MDN còn lưu ý một điểm: đặc tả yêu cầu performance.now() tiếp tục chạy trong lúc ngủ, nhưng trên thực tế có vẻ chỉ trình duyệt trên Windows làm vậy. Một đồng hồ đếm ngược phải kết thúc vào một giờ thực sau khi gập máy vì thế nên dùng Date.now(). Các đồng hồ trên trang này dùng Date.now(); cái giá là nếu đồng hồ máy được chỉnh trong lúc đang đếm, số đọc sẽ nhảy theo phần chỉnh đó.

Những lỗi thường gặp

  • Đếm nhịp. Mọi bộ đếm số lần gọi setInterval đều bị trôi, dù tab đang hiện hay không. Hãy lưu mốc bắt đầu hoặc kết thúc và tính từ đó.
  • Chỉ dùng requestAnimationFrame. Nó dừng ở tab bị ẩn, nên mọi thứ chỉ dựa vào nó (kể cả việc kiểm tra hết giờ) cũng dừng. Dùng nó để vẽ, không dùng để quyết định khi nào hết giờ.
  • Mong tiêu đề tab nhảy từng giây. Đếm ngược trên tiêu đề được cập nhật bằng bộ hẹn giờ, nên ở tab bị ẩn lâu có thể chỉ đổi mỗi phút một lần. Thời gian nó hiện vẫn đúng mỗi khi cập nhật.
  • Trông vào âm thanh mà chưa bấm vào trang. Trình duyệt chặn âm thanh bắt đầu mà không có thao tác của người dùng. Hãy thử chuông một lần và kiểm tra tab không bị tắt tiếng.
  • Đóng tab hoặc để máy ngủ. Trang web không thể reo sau khi đã đóng, và máy đang ngủ không phát gì cho đến khi thức dậy.
  • Tin vào phần trăm giây. Đồng hồ bấm giờ có thể hiển thị chúng, nhưng phản xạ của bạn lúc bấm bắt đầu và dừng thường là sai số lớn hơn.

Tóm lại

Trình duyệt cố ý làm chậm tab bị ẩn: nói chung khoảng mỗi giây một lần, ở Chrome là mỗi phút một lần sau năm phút yên lặng, và hoàn toàn dừng với khung hình hoạt ảnh. Đồng hồ đếm nhịp sẽ mất thời gian; đồng hồ lưu mốc thời gian thì không bao giờ. Đồng hồ bấm giờ chỉ cần một mốc thời gian. Đồng hồ đếm ngược còn cần một cách đáng tin cậy để thức dậy reo chuông: một timeout đơn lẻ đúng giờ và âm thanh hẹn trước trên đồng hồ âm thanh. Hãy thử cả hai trong đồng hồ bấm giờ và đếm ngược.

Nguồn: "Heavy throttling of chained JS timers beginning in Chrome 88", blog Chrome for Developers (developer.chrome.com/blog/timer-throttling-in-chrome-88); MDN Web Docs, "Window: setTimeout() method", mục "Reasons for delays longer than specified"; MDN Web Docs, "Performance: now() method".

Công cụ được nhắc trong bài

Về trang blog