jquery-timeago
jquery-timeago copied to clipboard
Performance Issue: setTimeout/setInterval
It makes more sense to use setTimeout instead of setInterval.
If this is done, the function can determine when the text is actually going to change and not waste calls that don't do anything. If the text says 2 years ago, it's probably not going to change in a minute, so it makes more sense to calculate the time delay.
Also, this would be more efficient for a modified version that shows %d seconds ago and needs to update at 1000 ms intervals only before it becomes a minute. Then it can change the interval to 60000 ms.
I see this as a genuine performance issue on a page which uses timeago on many elements.
Here's a version (based on a slightly older base) which does this, and also doesn't keep the timer running if the element is no longer in the DOM.
/*!
* Timeago is a jQuery plugin that makes it easy to support automatically
* updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
*
* @name timeago
* @version 1.4.0
* @requires jQuery v1.2.3+
* @author Ryan McGeary
* @license MIT License - http://www.opensource.org/licenses/mit-license.php
*
* For usage and examples, visit:
* http://timeago.yarp.com/
*
* Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
*
* Modified by EH
*/
var timeAgoId = 1;
(function(factory){
if(typeof define === 'function' && define.amd){
// AMD. Register as an anonymous module.
define(['jquery'], factory);
}else{
// Browser globals
factory(jQuery);
}
}(function($){
$.timeago = function(timestamp){
if(timestamp instanceof Date){
return inWords(timestamp);
}else if(typeof timestamp === "string"){
return inWords($.timeago.parse(timestamp));
}else if(typeof timestamp === "number"){
return inWords(new Date(timestamp));
}else{
return inWords($.timeago.datetime(timestamp));
}
};
var $t = $.timeago;
$.extend($.timeago, {
settings: {
refreshMillis: 60000,
allowPast : true,
allowFuture : false,
localeTitle : false,
cutoff : 0,
strings : {
prefixAgo : null,
prefixFromNow: null,
suffixAgo : "ago",
suffixFromNow: "from now",
inPast : 'any moment now',
seconds : "less than a minute",
minute : "about a minute",
minutes : "%d minutes",
hour : "about an hour",
hours : "about %d hours",
day : "a day",
days : "%d days",
month : "about a month",
months : "%d months",
year : "about a year",
years : "%d years",
wordSeparator: " ",
numbers : []
}
},
inWords: function(distanceMillis){
if(!this.settings.allowPast && !this.settings.allowFuture){
throw 'timeago allowPast and allowFuture settings can not both be set to false.';
}
var $l = this.settings.strings;
var prefix = $l.prefixAgo;
var suffix = $l.suffixAgo;
if(this.settings.allowFuture){
if(distanceMillis < 0){
prefix = $l.prefixFromNow;
suffix = $l.suffixFromNow;
}
}
if(!this.settings.allowPast && distanceMillis >= 0){
return this.settings.strings.inPast;
}
var seconds = Math.abs(distanceMillis) / 1000;
var minutes = seconds / 60;
var hours = minutes / 60;
var days = hours / 24;
var years = days / 365;
function substitute(stringOrFunction, number){
var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
var value = ($l.numbers && $l.numbers[number]) || number;
return string.replace(/%d/i, value);
}
var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
seconds < 90 && substitute($l.minute, 1) ||
minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
minutes < 90 && substitute($l.hour, 1) ||
hours < 24 && substitute($l.hours, Math.round(hours)) ||
hours < 42 && substitute($l.day, 1) ||
days < 30 && substitute($l.days, Math.round(days)) ||
days < 45 && substitute($l.month, 1) ||
days < 365 && substitute($l.months, Math.round(days / 30)) ||
years < 1.5 && substitute($l.year, 1) ||
substitute($l.years, Math.round(years));
var separator = $l.wordSeparator || "";
if($l.wordSeparator === undefined){
separator = " ";
}
return $.trim([prefix, words, suffix].join(separator));
},
parse : function(iso8601){
var s = $.trim(iso8601);
s = s.replace(/\.\d+/, ""); // remove milliseconds
s = s.replace(/-/, "/").replace(/-/, "/");
s = s.replace(/T/, " ").replace(/Z/, " UTC");
s = s.replace(/([\+\-]\d\d)\:?(\d\d)/, " $1$2"); // -04:00 -> -0400
s = s.replace(/([\+\-]\d\d)$/, " $100"); // +09 -> +0900
return new Date(s);
},
datetime: function(elem){
var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
return $t.parse(iso8601);
},
isTime : function(elem){
// jQuery's `is()` doesn't play well with HTML5 in IE
return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
}
});
// functions that can be called via $(el).timeago('action')
// init is default when no action is given
// functions are called with context of a single element
var functions = {
init : function(){
this.id = timeAgoId++;
var refresh_el = $.proxy(refresh, this);
refresh_el(true);
},
update : function(time){
var parsedTime = $t.parse(time);
$(this).data('timeago', { datetime: parsedTime });
if($t.settings.localeTitle) $(this).attr("title", parsedTime.toLocaleString());
refresh.apply(this);
},
updateFromDOM: function(){
$(this).data('timeago', { datetime: $t.parse($t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title")) });
refresh.apply(this);
},
dispose : function(){
if(this._timeagoInterval){
window.clearInterval(this._timeagoInterval);
this._timeagoInterval = null;
}
}
};
$.fn.timeago = function(action, options){
var fn = action ? functions[action] : functions.init;
if(!fn){
throw new Error("Unknown function name '" + action + "' for timeago");
}
// each over objects here and call the requested function
this.each(function(){
fn.call(this, options);
});
return this;
};
function refresh(first){
if (first || $(this).closest('body').length) {
var data = prepareData(this);
var $s = $t.settings;
var nextTime = $s.refreshMillis;
if(!isNaN(data.datetime)){
var dist = distance(data.datetime);
if($s.cutoff == 0 || dist < $s.cutoff){
$(this).text(inWords(data.datetime));
}
var seconds = Math.abs(dist) / 1000;
var minutes = seconds / 60;
var hours = minutes / 60;
var days = hours / 24;
var years = days / 365;
// If the next change is a long time away, set the timer appropriately.
if (years > 1) {
nextTime = 365 * 24 * 60 * 60 * 1000 / 2;
} else if (days > 1) {
nextTime = 24 * 60 * 60 * 1000 / 2;
} else if (hours > 1) {
nextTime = 60 * 60 * 1000 / 2;
} else if (minutes > 1) {
nextTime = 60 * 1000 / 2;
}
}
var refresh_el = $.proxy(refresh, this);
var $s = $t.settings;
if($s.refreshMillis > 0){
this._timeagoInterval = setTimeout(refresh_el, nextTime);
}
}
return this;
}
function prepareData(element){
element = $(element);
if(!element.data("timeago")){
element.data("timeago", { datetime: $t.datetime(element) });
var text = $.trim(element.text());
if($t.settings.localeTitle){
element.attr("title", element.data('timeago').datetime.toLocaleString());
}else if(text.length > 0 && !($t.isTime(element) && element.attr("title"))){
element.attr("title", text);
}
}
return element.data("timeago");
}
function inWords(date){
return $t.inWords(distance(date));
}
function distance(date){
return (new Date().getTime() - date.getTime());
}
// fix for IE6 suckage
document.createElement("abbr");
document.createElement("time");
}));
@edwh, I only took a quick look at your code, but I think it has a bug.
a year ago (1.4 years ago) becomes 2 years ago after 0.1 years (that's the current behavior because of rounding). However, it won't update until it becomes 2.4 years ago.
Also, it might not work for future dates.
a year from now updates after 1 month, not 1 year, to 11 months from now.
@theonlypwner Even with the / 2? I would expect that to cause it to check again after a further 0.5 years. But even then it should really update sooner than that if it was to preserve the switchover at 1.5 years, so you're right it's bugged.
For my immediate purposes that's ok - I'm only interested in something rough for past dates only, so if it's a minute or two out I don't much mind. The performance hit of having a zillion timers running was a killer - particularly using setInterval, because once you had more than a certain number of timers running it wouldn't be able to keep up and so would flatline in timer processing. I did wonder about trying to avoid having a per-usage timer at all, but, y'know, other fish to fry.
I may come back to this and do a less hasty fix, but don't hold your breath :-)
All %d years dates are updated at the same time.
It's probably better to group timers for yearly, monthly, daily, etc. updates.
After a timer is run, it could possibly become an interval.
Unless you can tolerate error of up to 1 year, disregard what I said about grouping timers, as 2 years ago for one timer could be supposed to update a few second after another timer that says the same. Updating all of the "year" timers at the same time could be more efficient (less timers), but less accurate.
The timeago.org plugin updates the timer ~~1 second after~~ immediately when it's supposed to change. ~~It makes sense to set the timer like this (but maybe 1 ms after the time instead of 1 entire second).~~
/**
* nextInterval: calculate the next interval time.
* - diff: the diff sec between now and date to be formated.
*
* What's the meaning?
* diff = 61 then return 59
* diff = 3601 (an hour + 1 second), then return 3599
* make the interval with high performace.
**/
function nextInterval(diff) {
var rst = 1, i = 0, d = Math.abs(diff);
for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
diff /= SEC_ARRAY[i];
rst *= SEC_ARRAY[i];
}
// return leftSec(d, rst);
d = d % rst;
d = d ? rst - d : rst;
return Math.ceil(d);
}
https://github.com/hustcc/timeago.js/blob/310864d574c89cb692c0395dd7fe650cfa12fba7/src/timeago.js#L80-L99