Symphonious

Living in a state of accord.

The Curious Case of Vuetify Bug 2773

I ran into an issue with Vuetify recently where the 1.0 betas were constantly utilising a CPU core when run in Safari. It turned out, I wasn’t alone and it had been reported as bug 2773.

There wasn’t a lot to go on – the problem only happened in Safari and the timeline showed it was constantly invalidating and recalculating the layout, apparently without any JavaScript executing.

First step was to create a simple reproduction case, which turned out to be a template with just:

<v-app>
  <v-navigation-drawer app></v-navigation-drawer>
</v-app>

So something to do with navigation drawers and whatever effects the app attribute causes. Looking at how the generated DOM changes with and without ‘app’ led me to discover the suspicious looking CSS:

max-height: calc(100% - 0px);

Sure enough, the problem went away if that was changed to a simple max-height: 100%; but the ‘- 0px’ part isn’t as silly as it seems. If you have a toolbar at the top of the page it adjusts to allow for the toolbar height becoming ‘max-height: calc(100% – 64px)’ which also causes high-CPU. But an element with ‘max-height: calc(100% – 0px)’ by itself doesn’t trigger the bug so there must be something else contributing to the problem…

More digging ensued, including deleting large chunks of Vuetify’s css to see when the problem went away which lead to the rule:

.navigation-drawer[data-booted="true"] {
  transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}

That and the max-height was all the CSS required to reproduce the problem when Vuetify created the DOM, but not enough when applied to a static HTML page.

The ‘data-booted’ attribute is added dynamically by Vuetify when the component is loaded to trigger an enter animation on the element but I still couldn’t reproduce the issue without Vuetify even if I wrote JavaScript to add that attribute after a delay.

It turns out the third piece required to trigger the high CPU usage is that the element has to be created with document.createElement. If it’s included in the page HTML or even created with el.outerHTML = ‘…’ there’s no issue.  So it turns out to be a Safari/WebKit bug which can be reproduced with:

  <div id="app">
  </div>
  <script>
    const nav = document.createElement('aside');
    nav.style.maxHeight = 'calc(100% - 0px)';
    const originalEl = document.getElementById('app');
    originalEl.parentElement.replaceChild(nav, originalEl);
    requestAnimationFrame(function() {
      nav.style.transitionDuration = '1s';
    });
  </script>

You can try it out.  With Safari 11.0.2 (and possibly other versions) you can see the Timelines tab in dev tools show the page constantly recalculating layout and Activity Monitor will show the process for that tab using around 100% CPU.

The work around for Vuetify was to ensure that transition-property which was defaulting to ‘all’ (because lots of things about navigation drawers animate) doesn’t include the ‘max-height’ property or anything else that uses ‘calc’. Hence pull request 2958.

The problem appears to have already been fixed in WebKit as I can’t reproduce it in the current Safari Technology Preview build.