Preface: This issue applies to the initialization of most Foundation library events, but it just happens to be most pronounced for us with tooltips since we have some scenarios where there are hundreds of tooltips being loaded on the page at a time. The following assessment is based on my basic understanding of the Foundation JavaScript libraries, so please correct me if I'm wrong.
We've been noticing some severe performance issues in Firefox on pages that have a large number of tooltips (200+) on them to the point that Firefox hangs indefinitely (other browser don't seem to have the issue). Even "Stop Script" won't stop it, and you'll ultimately have to force quit the browser.
The issue lies in the design of the events function and how Foundation lib bindings are initialized. From what I can see, most/all Foundation libraries have an init() method that invokes the base bindings() function. The bindings function then, by default, will call its internal bind() method once for each element in the DOM with the relevant data-* attribute (e.g. data-tooltip, data-reveal, etc.). The issue is that bind then calls events() on the library for the given $target element. In some cases, that target element isn't used (e.g. reveal) and in other cases, the target element is used to do some further initialization (e.g. in tooltip to create the hidden tooltip div).
Ultimately, however, every single element that matches the data-* attribute will result in the various event bindings being removed/re-created on the scope of that given instance (which is the document by default). So, if you have 200 tooltips on your page, the browser has to bind/remove/re-bind the same exact event handlers 200 times. This seems rather wasteful of CPU cycles and should be able to be optimized. Perhaps these types of global events need to be split out into a separate global event handler or each library should detect if the handlers have already been initialized (and if they have, avoid doing the initialization again)?
This is what is ultimately crashing Firefox when there are a lot of tooltips on the page. If the overall design is fixed/improved so that these event handlers only get bound a single time, that would definitively solve the problem, and likely also improve initialization performance of Foundation dramatically, too.
In terms of the actual events being bound in the tooltip library, it actually looks like the only event that is problematic in Firefox is the "DOMNodeRemoved DOMAttrModified" event. That event is added, but never removed since the .off() call on each iteration only applies to ".tooltip" events. Thus, those event handlers are potentially being added hundreds of times, depending on how many tooltips are on the page. It looks like that event being repeatedly added is what is ultimately crashing the browser. One quick solution would be to change that event to use "DOMNodeRemoved.tooltip DOMAttrModified.tooltip" which prevents Firefox from crashing. Of course, you still have the wasted CPU cycles for removing/re-binding those events hundreds of times, so it would probably be worthwhile to improve the overall design of the library event binding system, too.
I haven't created any pull requests for any of these changes since I figured there would need to be some kind of discussion/agreement regarding the best approach to take here. I would think that, at a minimum, the ".tooltip" event namespace should be added to those events (assuming that's cross-browser compatible).
I'm not as familiar with the overall design of Foundation, so there very well may be better options to the overall design issue than what I've suggested here, too.
Looking forward to hearing your thoughts!
Looks like a similar issue was logged here. https://github.com/zurb/foundation/issues/6092 While we can't vouch for 200 tooltips, 40-50 is a reasonable use case we solved for in Foundation 6.
@zurbchris I think this has been addressed in Foundation 6. Can you confirm?
Well, committing to supporting only 40-50 tooltips on a page while maintaining an acceptable level of performance is going to limit your users quite a bit. My recommended solutions above actually solve this issue when there are thousands of tooltips on the page, including in Firefox (which seems to have the biggest performance issues). It really does appear to be a fundamental design flaw in how the events are being registered in Foundation, so if the root of the design issue is fixed, then there should be no concerns with excessive numbers of tooltips.
For what it's worth, here's the updated Foundation.libs.tooltips.events method implementation that definitively solves this problem:
var isInitted = false;
Foundation.libs.tooltip.events = function (instance) {
var self = this,
S = self.S;
self.create(this.S(instance));
if(isInitted) {
return;
}
isInitted = true;
function _startShow(elt, $this, immediate) {
if (elt.timer) {
return;
}
if (immediate) {
elt.timer = null;
self.showTip($this);
} else {
elt.timer = setTimeout(function () {
elt.timer = null;
self.showTip($this);
}.bind(elt), self.settings.hover_delay);
}
}
function _startHide(elt, $this) {
if (elt.timer) {
clearTimeout(elt.timer);
elt.timer = null;
}
self.hide($this);
}
$(this.scope)
.off('.tooltip')
.on('mouseenter.fndtn.tooltip mouseleave.fndtn.tooltip touchstart.fndtn.tooltip MSPointerDown.fndtn.tooltip',
'[' + this.attr_name() + ']', function (e) {
var $this = S(this),
settings = $.extend({}, self.settings, self.data_options($this)),
is_touch = false;
if (Modernizr.touch && /touchstart|MSPointerDown/i.test(e.type) && S(e.target).is('a')) {
return false;
}
if (/mouse/i.test(e.type) && self.ie_touch(e)) {
return false;
}
if ($this.hasClass('open')) {
if (Modernizr.touch && /touchstart|MSPointerDown/i.test(e.type)) {
e.preventDefault();
}
self.hide($this);
} else {
if (settings.disable_for_touch && Modernizr.touch && /touchstart|MSPointerDown/i.test(e.type)) {
return;
} else if (!settings.disable_for_touch && Modernizr.touch && /touchstart|MSPointerDown/i.test(e.type)) {
e.preventDefault();
S(settings.tooltip_class + '.open').hide();
is_touch = true;
// close other open tooltips on touch
if ($('.open[' + self.attr_name() + ']').length > 0) {
var prevOpen = S($('.open[' + self.attr_name() + ']')[0]);
self.hide(prevOpen);
}
}
if (/enter|over/i.test(e.type)) {
_startShow(this, $this);
} else if (e.type === 'mouseout' || e.type === 'mouseleave') {
_startHide(this, $this);
} else {
_startShow(this, $this, true);
}
}
})
.on('mouseleave.fndtn.tooltip touchstart.fndtn.tooltip MSPointerDown.fndtn.tooltip', '[' + this.attr_name() + '].open', function (e) {
if (/mouse/i.test(e.type) && self.ie_touch(e)) {
return false;
}
if ($(this).data('tooltip-open-event-type') == 'touch' && e.type == 'mouseleave') {
return;
} else if ($(this).data('tooltip-open-event-type') == 'mouse' && /MSPointerDown|touchstart/i.test(e.type)) {
self.convert_to_touch($(this));
} else {
_startHide(this, $(this));
}
})
.on('DOMNodeRemoved DOMAttrModified', '[' + this.attr_name() + ']:not(a)', function (e) {
_startHide(this, S(this));
});
};
All I've added is the isInitted flag to prevent the events from being bound/unbound once for every single tooltip on the page (which is completely unnecessary). The events only need to be bound once for the entire DOM.
Hey @brianlenz, sorry for not getting back to you sooner! As Foundation 5 is at EOL, we're only implementing serious bug fixes, not doing new features or performance improvements.
_However_, you seem to have arrived at a solution, so if you're interested in submitting it as a pull request to the V5 branch, we'll gladly take a look. Thanks! :)
@gakimball Is this something that could be brought into v6?
brianlenz's solution works perfectly, thanks! Hi @gakimball, I don't think this is a minor bug. Tooltips are very useful in some cases and when these are used in lists, the amount of tooltips rise up to more than 100 easily. I'd like to know if migrating to v6 it would be solved (for Firefox) or you would consider to add this solution to v5.
@brianlenz, thx so much! Your investigation really helpful, I also have like 200-300 tooltips on page and on really good PC I was waiting for 30 sec. to get it initialized. After patching initializing time reduced to 1-2 seconds (with page loading and other stuff)
If you have version : '5.5.3' then you need to look for
Foundation.libs.tooltip = {
...
events : function (instance) {
var self = this,
S = self.S;
and change it to
isInitted : false,
events : function (instance) {
var self = this,
S = self.S;
self.create(this.S(instance));
if (this.isInitted) {
return;
}
this.isInitted = true;
Does anyone have a fork of the latest version with this fix? - It seems like a critical bug and it's a shame it won't be fixed. It makes Foundation 5 completely unusable on IE and Firefox and there are still no upgrade docs to 6 :(
EDIT: I created a branch with fixes for Rails, if you use Rails you can add this to your Gemfile:
gem 'foundation-rails', git: 'https://github.com/xanview/foundation-rails.git', branch: 'v5.0_fixes'
Most helpful comment
For what it's worth, here's the updated Foundation.libs.tooltips.events method implementation that definitively solves this problem:
All I've added is the isInitted flag to prevent the events from being bound/unbound once for every single tooltip on the page (which is completely unnecessary). The events only need to be bound once for the entire DOM.