Countdown timer 'delays' when tab is inactive?
Asked Answered
M

4

9

Trying to build a very simple Javascript countdown. However, whenever the tab is inactive, the countdown begins to lag and holds an incorrect count.

See jsfiddle here for example: https://jsfiddle.net/gbx4ftcn/

function initTimer(t) {

  var self = this,
    timerEl = document.querySelector('.timer'),
    minutesGroupEl = timerEl.querySelector('.minutes-group'),
    secondsGroupEl = timerEl.querySelector('.seconds-group'),

    minutesGroup = {
      firstNum: minutesGroupEl.querySelector('.first'),
      secondNum: minutesGroupEl.querySelector('.second')
    },

    secondsGroup = {
      firstNum: secondsGroupEl.querySelector('.first'),
      secondNum: secondsGroupEl.querySelector('.second')
    };

  var time = {
    min: t.split(':')[0],
    sec: t.split(':')[1]
  };

  var timeNumbers;

  function updateTimer() {

    var timestr;
    var date = new Date();

    date.setHours(0);
    date.setMinutes(time.min);
    date.setSeconds(time.sec);

    var newDate = new Date(date.valueOf() - 1000);
    var temp = newDate.toTimeString().split(" ");
    var tempsplit = temp[0].split(':');

    time.min = tempsplit[1];
    time.sec = tempsplit[2];

    timestr = time.min + time.sec;
    timeNumbers = timestr.split('');
    updateTimerDisplay(timeNumbers);

    if (timestr === '0000')
      countdownFinished();

    if (timestr != '0000')
      setTimeout(updateTimer, 1000);

  }

  function animateNum(group, arrayValue) {

    TweenMax.killTweensOf(group.querySelector('.number-grp-wrp'));
    TweenMax.to(group.querySelector('.number-grp-wrp'), 1, {
      y: -group.querySelector('.num-' + arrayValue).offsetTop
    });

  }

  setTimeout(updateTimer, 1000);

}

I'm unsure whether the problem lies with the animation, or with the JS code itself.

For clarification: I want the countdown to continue when the tab is inactive, or to 'catch up with itself' when the tab comes back in to focus.

I know that setTimeout and setInterval can cause issues with inactive tabs, but I'm not entirely sure how to fix this.

Any help would be much appreciated!

Moderator answered 1/10, 2016 at 14:48 Comment(9)
please remove any code not relevant to the specific problemRobertson
Possible duplicate of Why does JavaScript setTimeout lag when in another tab?Henpeck
hint: store end time as date and always calculate based on difference between now and endRobertson
@Henpeck that link is relevant but doesn't provide solution for this situation so it's not really a duplicateRobertson
Use the Task scheduler if you can use service workerEstevez
@Robertson The end time is 10 minutes from when the user visits; more of a timer than a 'countdown'. Pretty new to JS so unsure on how I should store that. Am I right in saying that, at the moment, I'm simply just counting down, rather than calculating the difference?Moderator
yes...that analysis is correct. You are relying on the interval to be exact whereas if the interval isn't exact subtracting one date from another date will beRobertson
@Estevez Using HackTimer at the moment. Doesn't work well on Safari though. On Chrome it works fantastically.Moderator
storing the time difference between your timer and date.now is the simplest approach to keep consistency (as mentioned above by @charlietfl)Baxie
K
6

For this you can use the HTML5 Visibility API for detecting if the browser tab is active or not. And use regular binding of event handlers for focus and blur for the browser window.

Basically you pause() the timeline when you blur out of the tab, and then play() when you give the tab refocus. Example of this in action:

http://codepen.io/jonathan/pen/sxgJl

// Set the name of the hidden property and the change event for visibility
var hidden, visibilityChange; 
if (typeof document.hidden !== "undefined") { // Opera 12.10 and Firefox 18 and later support 
   hidden = "hidden";
   visibilityChange = "visibilitychange";
} else if (typeof document.mozHidden !== "undefined") {
    hidden = "mozHidden";
    visibilityChange = "mozvisibilitychange";
} else if (typeof document.msHidden !== "undefined") {
    hidden = "msHidden";
    visibilityChange = "msvisibilitychange";
} else if (typeof document.webkitHidden !== "undefined") {
    hidden = "webkitHidden";
    visibilityChange = "webkitvisibilitychange";
}

// If the page is hidden, pause the video;
// if the page is shown, play the video
function handleVisibilityChange() {
   if (document[hidden]) {
      tl.pause();
   } else {
      tl.play();
   }
}

// Warn if the browser doesn't support addEventListener or the Page Visibility API
if (typeof document.addEventListener === "undefined" || typeof document[hidden] === "undefined") {
   // do nothing or throw error via alert()
   alert("This demo requires a browser, such as Google Chrome or Firefox, that supports the Page Visibility API.");
} else {
   // Handle page visibility change 
   // Pause timeline  
   tl.pause();
}

HTML5 Visibility Docs:

https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API

Regarding GreenSock Forum Topic:

http://forums.greensock.com/topic/9059-cross-browser-to-detect-tab-or-window-is-active-so-animations-stay-in-sync-using-html5-visibility-api/

Also in GSAP the equivalent for setTimeout() is delayedCall()

Provides a simple way to call a function after a set amount of time (or frames). You can optionally pass any number of parameters to the function too.

GSAP delayedCall(): http://greensock.com/docs/#/HTML5/GSAP/TweenMax/delayedCall/

//calls myFunction after 1 second and passes 2 parameters:
TweenMax.delayedCall(1, myFunction, ["param1", "param2"]);

function myFunction(param1, param2) {
    //do stuff
}

I hope this helps!

Kissable answered 3/10, 2016 at 15:23 Comment(1)
Thanks, Jonathan ;)Freesia
T
3

before all, i have to mention that, those countdown animation is truly outstanding and elegant dear sir! well done...

Now to the answers: as mentioned in many articles, and here is one of them:

setInterval(func, delay) does not guarantee a given delay between executions. There are cases when the real delay is more or less than given. In fact, it doesn’t guarantee that there be any delay at all.

Source: http://javascript.info/tutorial/settimeout-setinterval
and..

Most browsers apply performance improvements by reducing the priority of several tasks on inactive tabs or even some portions of the page that aren't on screen. Sometimes these improvements affects the executions of Javascript intervals as well.

Source: How can I make setInterval also work when a tab is inactive in Chrome?

as you can see, they mentioned that setInterval does not guarantee a given delay between executions even when the tab is active, and lets just assume that setTimeout (the one that you used) is also the same because they are relatively is "the same"

so what is the solution to this problem?
well, what you actually can do is check how many times has actually elapsed between the event, something like this Codepen

EDIT: as you have requested, here is a fiddle of your countdown with the fix, and i have //commented on the changes i made to the fiddle, so it would hopefully make it easier for you to understand. So now, when you paired the countdown to other timer (ex. your phone's timer) you should get the same result even when the tab is inactive or the frame rate slowdown.

if you have enjoyed my answer please consider mark it as "the answer" or at least up vote it ;)

Tussis answered 1/10, 2016 at 16:1 Comment(3)
Thanks for your kind words! This is super interesting. Do you know where I would implement that in to a script like mine? I'm pretty brand-new to JS!Moderator
This is super helpful, again, thank you! Although, it doesn't always work. Sometimes the timer reaches 0000, chimes and shows the reload button, like it's supposed to. Other times, it doesn't chime and loops, counting down from 60:00.Moderator
Umm i'm truly sorry but, after i try the fiddle about 5 times, i don't see the error you're describing. Also, if you are satisfied with my answer please mark it as "The Answer" by clicking the check mark next to my answer! it would be really appreciated..Tussis
G
0

The simplest way to ensure the timer stays correct when a user moves off the tab and then returns is to use session storage - to store the original starting time:

on the first load - get the local current datetime and store into session storage (note that will only accept a string - so you will have to stringify the value and then parse it out again upon retrieval).

when the tab loses focus that set start time will still be stored as startTime in ss. When the tab regains focus - have a function that gets the new current datetime, gets the stored datedtime from session storage and calculates the difference. Then the timer can be updated to the new reduced time.

Eg if its 10:00 am on page load - set the startTime into ss. Then the user spends 1 minute on the site and goes offsite for 5 minutes. Upon returning - the above described process will determine the current time is 10:06 and determine that its 6 minutes later than the start time so can update the timer as such.

This avoids the need for intervals and timers etc. You can take the specificity down to ms if needed. And also - I like your fiddle too.

Gadmon answered 2/10, 2016 at 0:28 Comment(0)
S
0

You can use this code. Each time the tab changes, it calculates the end time again and updates the counter.

let interval;
let duration=timeDifference(endTime(),nowDate())
updateTime();
$(window).focus(function () {
    clearInterval(interval)
    updateTime();
});

function updateTime() {
     interval = setInterval(function () {
        var timer = duration.split(':');
        //by parsing integer, I avoid all extra string processing
        var hours = parseInt(timer[0], 10);
        var minutes = parseInt(timer[1], 10);
        var seconds = parseInt(timer[2], 10);
        --seconds;
        minutes = (seconds < 0) ? --minutes : minutes;
        if (minutes < 0) clearInterval(interval);
        seconds = (seconds < 0) ? 59 : seconds;
        seconds = (seconds < 10) ? '0' + seconds : seconds;
        hours = (hours < 10) ? '0' + hours : hours;
        //minutes = (minutes < 10) ?  minutes : minutes;
        $('.countdown').html(hours + ':' + minutes + ':' + seconds);
         duration = hours + ':' + minutes + ':' + seconds;
    }, 1000);
}
function nowDate() {
    let date = new Date()
    let time1 = new Date();
    date = date.toISOString().slice(0, 10);
    date = date.split('-');
    return new Date(date[2] + '/' + date[1] + '/' + date[0] + " " + time1.getHours() + ":" + time1.getMinutes() + ":" + time1.getSeconds());
}
function endTime() {
    let endTime = $('input[name=end_date]').val();
    endTime = endTime.split(' ');
    let date = endTime[0].split('-');
    let time = endTime[1];
    return new Date(date[2] + '/' + date[1] + '/' + date[0] + " " + time);
}
function timeDifference(date1,date2) {
    var difference = date1.getTime() - date2.getTime();

    var daysDifference = Math.floor(difference/1000/60/60/24);
    difference -= daysDifference*1000*60*60*24

    var hoursDifference = Math.floor(difference/1000/60/60);
    difference -= hoursDifference*1000*60*60

    var minutesDifference = Math.floor(difference/1000/60);
    difference -= minutesDifference*1000*60

    var secondsDifference = Math.floor(difference/1000);

    return hoursDifference +":"+minutesDifference+":"+secondsDifference
}
Snavely answered 11/1, 2022 at 15:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.