Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Line Numbers resize event going crazy when scrolling on mobile, freezing screen for more than a second #2123

Closed
ghost opened this issue Nov 27, 2019 · 21 comments · Fixed by #2125
Labels

Comments

@ghost
Copy link

ghost commented Nov 27, 2019

Information:

  • Prism version: PrismJS 1.17.1
  • Plugins: Line Numbers
  • Environment: Grunt, BrowserSync, Ubuntu.
  • ~2800 DOM elements, some long highlighted codes, no less than 12 pre>code highlighted.

Description
This line:

window.addEventListener('resize', function () {
    Array.prototype.forEach.call(document.querySelectorAll('pre.' + PLUGIN_NAME), _resizeElement);
});

It is firing aggressively on mobile (Real, !emulated) browser scroll events, freezing the screen for a few seconds. Without this event listener, scrolling became smooth again, but sometimes because of the large DOM I can see some lag when scrolling up and down, but not so bad like with this event listener.

With event listener 'resize':
With/without web workers the rendering lag is crazy long. At least with web workers I was able to scroll, couldn't see anything because of the slow rendering, but was able to scroll.

Without event listener 'resize':
With/without web workers there was no difference on the smoothness of scrolling.

Why this event?

@ghost
Copy link
Author

ghost commented Nov 27, 2019

I do not have enough knowledge of why use this event, but in my setup (core + line numbers) is working fine without this event. Resizing or not the screen, I do not have dynamic stuff being loaded, and if I had, I would call to highlight only that part. My pre code have this style:

pre,
pre > code {
    white-space: pre-wrap;
}
pre {
    overflow-x: auto;
    display: block;
}

I couldn't find a reason for this event, unless another plugin depend on it, and if this the case, would be a a wise to enable this event only when the other plugin is in use to avoid serious performance degradation on mobile.

@mAAdhaTTah
Copy link
Member

The thing I find surprising here is that the listener fires while you're scrolling... What browser are you using? And what phone/device is it on?

@ghost
Copy link
Author

ghost commented Nov 27, 2019

iPhone 6s, safari

@ghost
Copy link
Author

ghost commented Nov 27, 2019

may be related to this: https://stackoverflow.com/a/27966414/2391290

@mAAdhaTTah
Copy link
Member

That question is for mobile Chrome. I did find this one tho:

https://stackoverflow.com/questions/8898412/iphone-ipad-triggering-unexpected-resize-events/24212316

Answer suggests caching the current element size and only rerunning if it changes. Another answer suggests adding the viewport meta tag. Could you add that meta tag on your site and see if that helps at all?

@ghost
Copy link
Author

ghost commented Nov 27, 2019

meta tag does not solve, mine is responsive design, that solution needs width set on the meta tag. But, anyway, for what purpose that resize event is for? because is working fine when removing that event.

@mAAdhaTTah
Copy link
Member

I didn't write the code myself, but my assumption is it's needed to ensure the line number stay aligned as the browser resizes, which is relevant if you rotate the phone (for mobile) or resize the browser (for desktop).

@ghost
Copy link
Author

ghost commented Nov 27, 2019

Line numbers stay aligned without that event, during resize, rotate, refresh if:

pre,
pre > code {
    white-space: pre-wrap;
}

If white-space:pre than you need this event to align stuff, but changing to white-space:pre-wrap eliminate the need for this event, plus avoid something that this event will not fix: a long line of code without space, wider than parent width, line numbers will not be aligned. Also avoid performance bottleneck.

@RunDevelopment
Copy link
Member

Line numbers stay aligned without that event, during resize, rotate, refresh if:

No, it doesn't. I just tested this on the plugin page (aka. I resized it with the event disabled) and expectedly it didn't work because of how Line Number works.

But I do agree that the massive performance hit is not justified for such a simple plugin. The main problem is that the layout of the page is calculated for every line to get the height of each line.
So any ideas for how to minimize the layout calculations aside from "only resize when the width changed"?

@RunDevelopment
Copy link
Member

Assuming a monospace font, we know that a line doesn't have any breaks if it has <= characters compared to the longest line without breaks.

I believe this is the best we can do and it's largely ineffective for small screens...

@RunDevelopment
Copy link
Member

I made a test page with 6 code blocks and did a few tests:

Current approach: 3s
Batch per element: 400ms
Batch all elements: 300ms

The "batch per element" works by wrapping all each code lines with an element, appending all elements to the current sizer and then getting the height of all lines. Like this, all getBoundingClientRect calls are batched together without any mods to the DOM allowing the browser to optimize. So instead of many small layout calcs, we're doing one bigger one per element.

The "batch all elements" take this to the extreme and batches all elements together, so that the layout has to be calculated only once.

But this is still not optimal as a full layout calculation for the whole page actually takes 80ms.
That's because there are about twice as many elements during the layout calculation of the resize, so that calc takes ~160ms. But before calculating the layout, the browser needs 100ms to apply the style information to all the newly created elements which are our lines.

So there's room for further optimization but it's 10x faster already.

@ghost
Copy link
Author

ghost commented Nov 29, 2019

I made a test page with 6 code blocks and did a few tests:

Current approach: 3s
Batch per element: 400ms
Batch all elements: 300ms

The "batch per element" works by wrapping all each code lines with an element, appending all elements to the current sizer and then getting the height of all lines. Like this, all getBoundingClientRect calls are batched together without any mods to the DOM allowing the browser to optimize. So instead of many small layout calcs, we're doing one bigger one per element.

The "batch all elements" take this to the extreme and batches all elements together, so that the layout has to be calculated only once.

But this is still not optimal as a full layout calculation for the whole page actually takes 80ms.
That's because there are about twice as many elements during the layout calculation of the resize, so that calc takes ~160ms. But before calculating the layout, the browser needs 100ms to apply the style information to all the newly created elements which are our lines.

So there's room for further optimization but it's 10x faster already.

That is good news, faster is better. I will try to get a combination of CSS that I'm using that does not need lines being re aligned, just need on load and nothing else after. Things just stay in place on resize or orientation change without any other interaction.

@ghost
Copy link
Author

ghost commented Nov 29, 2019

I have changed all units from em to rem that are provided by prismjs and changed a few things. I have my HTML set to 1rem. I removed colors and background choice to reduce lines from the code below:

code,
pre,
pre > code {

  font-size: 1rem;
  font-family: monospace;
  line-height: 1.6;
  vertical-align: middle;
  text-align: left;

  -webkit-hyphens: none;
  -moz-hyphens: none;
  hyphens: none;

  -moz-tab-size: 4;
  tab-size: 4;

  white-space: pre-wrap;
}

pre {
  padding: 1rem;
  overflow-x: auto;
  display: block;
}

pre > code {
  min-width: 700px;
  max-width: 900px;
  text-align: left;
  border: 0;
  padding: 0;
  margin: 0;
}

code {
  display: inline-block;
  text-align: center;
  margin: 0 0.15rem;
  padding: 0.08rem 0.3rem;
}

pre[class*="language-"].line-numbers {
    position: relative;
    padding-left: 3.8rem;
    counter-reset: linenumber;
  }

  pre[class*="language-"].line-numbers > code {
    position: relative;
    white-space: inherit;
  }

  .line-numbers .line-numbers-rows {
    position: absolute;
    pointer-events: none;
    top: 0;
    font-size: 100%;

    left: -3.8rem;
    width: 3rem; // works for line-numbers below 1000 lines
    letter-spacing: -1px;
  }

  .line-numbers-rows > span {
    pointer-events: none;
    display: block;
    counter-increment: linenumber;
  }

  .line-numbers-rows > span:before {
    content: counter(linenumber);
    display: block;
    padding-right: 0.8rem;
    text-align: right;
  }

No other code for pre and code is applied. In this case, my layout does not need to be recalculated. On resize or orientation change or zooming.

@ghost
Copy link
Author

ghost commented Nov 29, 2019

For this context I think rem is a better choice than em.

@RunDevelopment
Copy link
Member

In this case, my layout does not need to be recalculated. On resize or orientation change or zooming.

How does your layout not change under a resize? You only set a max-width, so the width of a code block might have changed when the window resizes.
Could you please explain what you mean?

@ghost
Copy link
Author

ghost commented Nov 29, 2019

with that css, rem instead of em, and pre-wrap instead of pre:

code,
pre,
pre > code {
    white-space: pre-wrap;
}

there is no need to have

window.addEventListener('resize', function ()...

line numbers stay in place, aligned.

@ghost
Copy link
Author

ghost commented Nov 29, 2019

anyway, safari, maybe other browser too, trigger resize on mobile a lot, during scroll because of the hide bar or simple shrink it, and that is why if is going to listen on resize, better have an if inside to make sure the width really changed. I saw some explanations on stackoverflow about it, but I have to test some of than. Instead of:

window.addEventListener('resize', function () {
        Array.prototype.forEach.call(document.querySelectorAll('pre.' + PLUGIN_NAME), _resizeElement);
        resizeElements(Array.prototype.slice.call(document.querySelectorAll('pre.' + PLUGIN_NAME)));
});

something like this:

window.addEventListener('resize', function () {
    if(widthChanged){
        Array.prototype.forEach.call(document.querySelectorAll('pre.' + PLUGIN_NAME), _resizeElement);
        resizeElements(Array.prototype.slice.call(document.querySelectorAll('pre.' + PLUGIN_NAME)));
    }
});

but in my case, if I'm going to deploy on production, i will fork prismjs, comment out this event, and handle that only with plain CSS, and that what is working for me.

@RunDevelopment
Copy link
Member

with that css, rem instead of em, and pre-wrap instead of pre:

rem means "root em", so the font-size of the HTML element. pre-wrap means "enable column wrap" and pre mean "disable column wrap".
So please explain to me how this means that the width of your code block stays constant during a resize where the width of the window changes e.g. orientation change. I understand the case where only the height changes but not when the width changes as well.


have an if inside to make sure the width really changed

I don't whether want to do this because some people like to use vh as their font-size unit of choice.

@ghost
Copy link
Author

ghost commented Nov 30, 2019

the performance degradation on mobile using this approach (listen on 'resize'), this should be an alternative, not the default... anyway, going to implement by my self something that does not kill the performance on mobile, just because someone may be using vh (this is insane, most of the traffic is on mobile this days).

@RunDevelopment
Copy link
Member

With the vh example I just wanted to point out that we can't just assume a certain design.
If we were to ignore this, Prism's line numbers would stop working correctly for some people from one minor version to another.

But you're right. We should do this optimization.

The problem is that we can't detect which units are used using getComputedStyle(e).fontSize as it converted to px. Are there any other ways?
If there aren't then we can just add a boolean Prism.plugin.lineNumbers.assumeIndependentUnits = true (name TBD).

@RunDevelopment
Copy link
Member

I added it to #2125 but it will default to false to ensure backward compatibility.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants