How did we make Hotjar work with styled-components?

Kris PinterMatthew BeasleyMay 31, 2018
2 min read

As part of our new front-end stack we’ve switched to using Styled Components; we’ve been very happy with how this has improved our workflow - we’ll be blogging about this soon!

Release 3.1 of styled components brought a significant performance improvement by switching how CSS was injected into the page; insertRule replaced appendChild. The only reason we know this is that we have just added Hotjar to our site to help UX get deeper insights into our users behaviour.

Hotjar is one of a suite of tools which listen and record DOM events which can be used to accurately play back a users session; and this is where insertRule becomes a problem - it raises no event Hotjar can listen to so when playing sessions back the styles are completely absent.

Broken Hotjar

We have raised the issue with Hotjar and they accept it’s a problem but could offer no solution so we had to go looking for ourselves.

We found this snippet which gave us the inspiration to make our own tweaked version…

Our main concern was performance, we are applying CSS rules twice - how will older browsers or particularly mobile devices cope? To reduce any impact we added a few additions

  1. Proxy insertRule - don’t periodically check for changes - listen to the actual calls being made.
  2. Only run the code when Hotjar is recording.
  3. Debounce appendChild so it doesn’t trigger lots of updates per second.

Here’s the snippet we ended up with - we’d love to hear what you think and if there’s any more tweaks we can make to it.

var syncStylesEl = document.createElement('style')
syncStylesEl.type = 'text/css'
document.head.insertBefore(syncStylesEl, document.head.children[0])

var syncHotJarStylesTimeout;
function debouncedSyncHotJarStyles() {
    var later = function() {
        var styleNodes = [].slice.call(document.querySelectorAll('head [data-styled-components]'))
        if (!styleNodes.length) { return }
        var styles = styleNodes
        .map(({ sheet }) => [].slice.call(sheet.cssRules)
             .map(rule => rule.cssText)
             .join(' ')
            )
        .join(' ')
        syncStylesEl.textContent = styles;
    };
    clearTimeout(syncHotJarStylesTimeout);
    syncHotJarStylesTimeout = setTimeout(later, 1000);
};

var originalInsertRule = CSSStyleSheet.prototype.insertRule;
CSSStyleSheet.prototype.insertRule = function (style, index) {
    originalInsertRule.call(this, style, index);
    if (window.hj && window.hj.settings && window.hj.settings.record) {
        debouncedSyncHotJarStyles();
    }
}
Kris Pinter
Front-End Engineer
Matthew Beasley
Matthew Beasley
Principal Front-End Engineer