Open afshin opened 2 years ago
@krassowski This looks interesting to me, I can try your proposed approach, let me know if you are already working on this issue.
I checked with @krassowski and he said he wasn't working on this. I'll assign it to you. Please feel free to hand it off back to me or someone else if you wish. I initially thought I'd make an attempt at it, but I'd be happier for you to go for it!
@3coins also please take a look at the original thread where this issue comes from because it has other approaches that were suggested as well: https://github.com/jupyterlab/jupyterlab/issues/9757#issuecomment-999915671
More fundamentally we are interested in reducing the "Recalculate Style" time for JupyterLab when a lot of DOM nodes are present. The virtual windowing (https://github.com/jupyterlab/jupyterlab/pull/12554) will help a lot by reducing the amount of nodes in the DOM.
An exploration that I would suggest for Lumino 2.0 is to see if we can narrow down the cause of style recalculation taking such a long time. This Google Dev article says that the cost is roughly 50:50 between selector matching and style computation. Is this because of the styles in JupyterLab, or is it because any styles in Lumino? If there are any styles in Lumino which need changing (e.g. to follow the BEM model), we should strive to adopt those in Lumino 2.0 as those (most likely) will be breaking changes.
I don't know how to profile what contributes to "Recalculate Style" cost in Chrome. The last time I looked there was no support for it in the Chrome profiler. I know that the Firefox Nightly has more granularity and it could help here. If everything else fails, we could just disable styles one-by-one and see what is the impact on performance.
Another possibility is to reconsider why is the "Recalculate Style" triggered. It is only triggered when there is a DOM mutation which could cause style invalidation (or browser wrongly thinks it could cause style invalidation). Currently we are attaching menu to the body and then removing it which is a blatant DOM mutation.
Would it make a difference if we always had a menu node in the body instead of adding and removing it? Maybe not because we still would be modifying its position, but maybe yes because the browser's heuristic would recognise that it is an innocuous change?
Would it make a difference if we always had a menu node in the body instead of adding and removing it? Maybe not because we still would be modifying its position, but maybe yes because the browser's heuristic would recognise that it is an innocuous change?
Of note, we already do that with completer in core, and completer does not have this problem. Maybe worth more exploration than I initially thought?
Synced up with @krassowski last week about this issue to get some more clarity. I spent some time last week replicating the issue locally on JL3, visually I wasn't able to see any perceived latency with the attached notebook open. I also attempted to investigate the performance within Chrome and Firefox, and did notice some "long tasks" pointing to the "recalculate style". My next step would be to download Firefox nightly and see if I can find any new information regarding "Recalculate Style". If this does not work, I am going to look into setting some automation to remove styles one-by-one and measure performance.
@3coins are you still working on this one? If not, I could take it up. The suggested workaround with a transparent div would also help with an issue I noticed of the menu not getting hidden when clicking on some widgets (notably tabbar, strangely it only happens 3/4 times):
@krassowski Plz feel free to take over, I have been busy with other things. Thanks.
The problem with hover boundary between menu and other widgets was narrowed down to an issue in Chromium style invalidation strategy and and worked around downstream in https://github.com/jupyterlab/jupyterlab/pull/13159. This helped by reducing style recalculation cost by orders of magnitude in Chromium.
Another problem we can tackle (highlighted in the original issue) is slow menu switching. While it is much better with the patch, it still performs hit-testing twice (and style recalculation thrice!). This affects all browsers, but after applying the patch from 13159 it is only a noticeable problem (0.5s) on a page with 300k nodes (100k of each: span, svg and div) and 6x slowdown.
A likely solution would be to either to:
in both cases it means creating a special method for switching the menu in the menu bar, rather than relying on existing sequence which is:
On mouse move reaching new menu bar entry (hit test)
activeIndex
(DOM modification)getBoundingClientRect
(DOM query)getBoundingClientRect
The ideal order of operations to maximise performance is:
getBoundingClientRect
(DOM modification)This way we would avoid spurious the reflows.
I also experimented with moving all menu nodes from document.body
to a single div
(with css contain
) or into shadow DOM (since that would be more practical than moving other widgets to shadow DOM, as extensions do not generally provide custom styles for menu) but the results so far were inconclusive.
If someone wants to test it out or carry on, see https://github.com/krassowski/lumino/commit/22367407e146fece16244c795d406f77a9fa6352
I performed initial analysis of the remaining contributors to the cost of style computation and layout, in plain JupyterLab (latest development version - 4.0.0a29), on Chromium with a notebook containing 300k nodes (100k svg, div and span):
tabbar.css
(lumino): 85.2% fastermenubar.css
(lumino): 4% faster (but of course we need that one)menus.css
(JupyterLab): 3.9% faster.lm-TabBar
: 78.3% faster.lm-MenuBar-content
3.8% faster.lm-MenuBar.lm-mod-active .lm-MenuBar-item.lm-mod-active
3.3% faster.fa-ul > li
1.1% faster
fa-
prefix.tabbar.css
(lumino) 82.4% fastermenus.css
(JupyterLab): 4.1% fastermenubar.css
(lumino): 3.3% fasterThe most promising styles to act on are:
.lm-TabBar {
display: flex;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
and
.lm-MenuBar-content {
margin: 0;
padding: 0;
display: flex;
flex-direction: row;
list-style-type: none;
}
The first one may corresponds to another performance issue in Chromium: https://bugs.chromium.org/p/chromium/issues/detail?id=605419, see this comment from 2017:
- Applying user-select:none on a top element having much children and clicking on it can cause performance issue because Chrome search 'clickable' element traversing the tree.
If it is significant problem, please submit as new issue (maybe titled "clicking on user-select:none is slow"?) since this is standerdize tracking issue.
however, I could not find such an issue so probably it was not acted on.
Interestingly both also include display: flex
which supposedly is faster than old layouts but I also know that lists are slow in Chromium so maybe this is not the case here.
Thanks a lot for the analysis and sharing the results.
Problem
In https://github.com/jupyterlab/jupyterlab/issues/9757#issuecomment-999915671 @krassowski writes:
Potential solution
In a further comment, @krassowski proposes