Loading fonts blocks browser when the page contains many elements

104 views
Skip to first unread message

jahvascr...@gmail.com

unread,
Jul 20, 2017, 11:12:55 AM7/20/17
to MathJax Users
Hello,

When the page contains many elements (about 100 000), it seems that simply loading a new font blocks the page (impossible to scroll, not reacting to clicks, for about a second) in Chromium 57 and Firefox 51 on Linux, with the latest MathJax (2.7.1).

My actual use case is a large document which contains lots of syntax-highlighted code (which generates a lot of small `<span>` elements), and math pre-rendered on the server side with mathjax-node. I'm using the pre-rendered math as MathJax_Preview elements, and re-rendering in the browser to get the right-click menu and better alignment of unicode characters (MathJax can't guess their size on the server). With the highlighted code Chromium blocks for about 0.4s to 0.6s when loading each font, and with the pre-rendered math, it blocks for 5s to 7s.

Small example (copied below): https://jsfiddle.net/hkwa6r8r/

If I insert the math in an <iframe> and run MathJax there, the fonts are loaded instantly, despite the hundred thousand of elements outside of the iframe: https://jsfiddle.net/mg6gwuh6/

This seems like a browser issue. By profiling the code, it first seemed that reading from this.div.offsetWidth in jax.js was causing the lag. But I inserted a window.setTimeout(…, 1000) before and adjusted the code accordingly to let enough time for the font to be loaded, and the profiler indicates that the blocked time is spend entirely in the browser's native code (i.e. it says “Program” with an empty stack, instead of pointing to an actual JavaScript function).

The lag actually occurs to some extent with the static, pre-generated math: the math first appears with incorrect fonts, and it is impossible to scroll smoothly while it is loading the fonts.

Since I can know ahead of time (when generating the static page on the server) which fonts are going to be needed, is there a way for me to force the fonts to be loaded early on (e.g. so that they are loaded before the page even reaches the <body> tag)? Or is there a trick with iframes that I could use?

Thanks,
Georges Dupéron



Code for the first jsfiddle:

<!DOCTYPE html><title>Example</title>
<script type="text/javascript">
// Loads the following fonts: "MathJax_AMS", "MathJax_Caligraphic", "MathJax_Main", "MathJax_Main-bold", "MathJax_Main-italic", "MathJax_Math-italic", "MathJax_Script", "MathJax_Size2", "MathJax_Size3", "MathJax_Typewriter"
var mymath = '\\[\\mathbb{A}\\mathcal{A}\\textrm{a}\\textbf{a}\\textit{a}a\\mathscr{A}\\bigcup\\binom{a}{a}\\texttt{A}\\]';
var manyElements = '';
for (var i = 0; i < 1000; i++) {
  manyElements += '<p>'
  for (var j = 0; j < 100; j++) {
    manyElements += '<span>'+i+''+j+'</span>';
  }
  manyElements += '</p>'
}
document.write(mymath+manyElements);
</script>
<script type="text/javascript"
</script>
<script type="text/x-mathjax-config">
  MathJax.Hub.Config({
    "fast-preview": { disabled: true, },
  });
</script>




Code for the second jsfiddle:

<!DOCTYPE html><title>Example</title>
<iframe id="ifrm"></iframe>
<script type="text/javascript">
// Loads the following fonts: "MathJax_AMS", "MathJax_Caligraphic", "MathJax_Main", "MathJax_Main-bold", "MathJax_Main-italic", "MathJax_Math-italic", "MathJax_Script", "MathJax_Size2", "MathJax_Size3", "MathJax_Typewriter"
var mymath = '\\[\\mathbb{A}\\mathcal{A}\\textrm{a}\\textbf{a}\\textit{a}a\\mathscr{A}\\bigcup\\binom{a}{a}\\texttt{A}\\]';
var manyElements = '';
for (var i = 0; i < 1000; i++) {
  manyElements += '<p>'
  for (var j = 0; j < 100; j++) {
    manyElements += '<span>'+i+''+j+'</span>';
  }
  manyElements += '</p>'
}
var ifrm = document.getElementById("ifrm");
var ifrmw = ifrm.contentWindow;
if (!ifrmw) { ifrmw = (ifrm.contentDocument.document || ifrm.contentDocument); }
ifrmw.document.open();
ifrmw.document.write(mymath + '<scr' + 'ipt type="text/javascript"'
   + '</scr' + 'ipt>'
   + '<scr' + 'ipt type="text/x-mathjax-config">'
   + 'MathJax.Hub.Config({'
   + '"fast-preview": { disabled: true, },'
   + '});'
   + '</scr' + 'ipt>');
ifrmw.document.close();
document.write(manyElements);
</script>

Dupéron Georges

unread,
Jul 20, 2017, 9:29:14 PM7/20/17
to mathja...@googlegroups.com
It seems two things can be done to solve this issue:

1) The lag which occurs when a font is loaded depends on how many elements are displayed. I tried wrapping everything in a <div id="wrapper">…</div>, setting display:none; on that div, rendering a simple equation with characters from all the needed fonts, and turning display:block; back on for the wrapper. This made the font-loading step much more smooth, and it is then possible to render the other real equations in the page. By waiting for the body to be fully loaded, and forcing its current height as a min-height, I was able to avoid scrolling issues when toggling off and back on the wrapper div.

2) For pre-rendered math (using mathjax-node), setting font-display: block; on every generated @font-face makes a local copy of the page load instantly, whereas it was being poorly responsive for several seconds without.

Unfortunately for live math, even with the fonts pre-loaded the MathJax HTML-CSS renderer is too slow to get smooth scrolling while it's busy. CommonHTML is a bit better, but still causes some jitter while scrolling. I tried forcing the width and height of a container around the math, with its expected size, in the hope that it would prevent reflows affecting the entire page when an equation is typeset, but I did not notice any improvement.

I think I'll use only the pre-rendered math with font-display: block; for now, and place a button at the top of the page allowing users to enable MathJax, if they want to take advantage of the extra options it provides (zoom, speech, …).

Davide Cervone

unread,
Jul 24, 2017, 2:51:06 PM7/24/17
to mathja...@googlegroups.com
You are right that the issue is related to the number of elements being displayed, and the more there are the slower it will be, and that it is related to the loading of web fonts.  What I suspect is happening is that when an @font-face directive for a web font is inserted into the page, the browser needs to look through the CSS of all elements to see if any of them need to be updated based on the newly requested font, and if so, the page needs to be played out again (by inserting either blank space or a default font until the web font is loaded).  With hundreds of thousands of elements, that can take time, particularly if the layout changes.  (Indeed, just displaying the 100,000 spans initially takes about 4 seconds for me, so a reflow could be expensive.)

I was going to suggest the wrapper div approach that you describe, though I would have started with it having display:none and only making it visible after rendering the single math element, which I would put into an absolutely positioned div of height 1px with overflow:hidden, so that the expression is not shown on screen (but still renders), and then using MathJax.Hub.Queue() to queue a function that removed the display:none from the wrapper.  That way you don't get the scrolling issues you mention.  The reason this works is that elements with display:none are not played out but he browser, and so changes to CSS don't propagate to them, and so the new @font-face directives don't have to be checked against them.

There is another approach that might work better for you, however.

If I understand correctly, your mathjax-node output is HTML (rather than SVG), so your previews are using the CommonHTML output.  For that, you should be including the CSS that you obtained from mathjax-node so that the will format properly.  That will include the @font-face CSS needed for CommonHTML output, and since it will be in the page already at the outset, if you use the CommonHTML output for the in-page re-rendering (rather than the HTML-CSS output that your page is configured to use), there is no need for new @font-face CSS to be inserted into the page, and you can avoid the changes that are causing the slow page reflows entirely.

In order to do that, you need to insert a bit of configuration that modifies the styles that CommonHTML inserts into the page to remove the ones already in the page due to the CSS you inserted from mathjax-node.  If you add

<script type="text/x-mathjax-config">
  MathJax.Hub.Register.LoadHook("[MathJax]/jax/output/CommonHTML/fonts/TeX/fontdata.js",
    function () {
      var STYLES = MathJax.Hub.config.CommonHTML.styles;
      MathJax.Hub.config.CommonHTML.styles = {
        "#MathJax_CHTML_Tooltip": STYLES["#MathJax_CHTML_Tooltip"],
        ".MJXc-processing": STYLES[".MJXc-processing"],
        ".MJXc-processed": STYLES[".MJXc-processed"],
        ".mjx-chartest": STYLES[".mjx-chartest"],
        ".mjx-chartest .mjx-char": STYLES[".mjx-chartest .mjx-char"],
        ".mjx-chartest .mjx-box": STYLES[".mjx-chartest .mjx-box"],
        ".mjx-test": STYLES[".mjx-test"],
        ".mjx-ex-boxtest": STYLES[".mjx-ex-boxtest"]
      };
    }
  );
</script>

to your page BEFORE the script that loads MathJax.js itself, that should do it (mathjax-node removes a few styles that CommonHTML in the browser uses to make measurements of things, so this code puts those back, and removes all the others that you will have already included explicitly).

That should make the page load without delays due to changing CSS for web fonts.

2) For pre-rendered math (using mathjax-node), setting font-display: block; on every generated @font-face makes a local copy of the page load instantly, whereas it was being poorly responsive for several seconds without.

It seems reasonable to include that in your static CSS from mathjax-node.

Unfortunately for live math, even with the fonts pre-loaded the MathJax HTML-CSS renderer is too slow to get smooth scrolling while it's busy. CommonHTML is a bit better, but still causes some jitter while scrolling.

There are several configuration parameters that control how responsive MathJax is while it is rendering the page, and allow you to trade off overall rendering speed for UI responsiveness.  For pages with large numbers of equations, you might need to adjust these for better scrolling during rendering, at the cost of taking longer to render all the equations.  In your case, since you have pre-rendered equations, that should not be a problem.

The parameters are the "chunking" parameters, EqnChunk, EqnChunkFactor, and EqnChunkDelay.

EqnChunk: 50
 
EqnChunkFactor: 1.5
 
EqnChunkDelay: 100

These values control how “chunky” the display of mathematical expressions will be; that is, how often the equations will be updated as they are processed.

EqnChunk is the number of equations that will be typeset before they appear on screen. Larger values make for less visual flicker as the equations are drawn, but also mean longer delays before the reader sees anything.

EqChunkFactor is the factor by which the EqnChunk will grow after each chunk is displayed.

EqChunkDelay is the time (in milliseconds) to delay between chunks (to allow the browser to respond to other user interaction).

Since Javascript is single-threaded, if a javascript program is running, the user interface is blocked.  So for something like MathJax that can run for a long time, in order to allow the user to do things like scroll the page or even to let the browser to update the typeset equations on the page, MathJax just voluntarily give up the CPU by pausing for a bit.  These parameters control how often MathJax pauses, and for how long.

The EqnChunk value says how many equations to typeset before pausing, and EqnCHunkDelay says how long to pause between chunks.  The longer the delay and the smaller EqnChunk is, the more responsive the UI should be.  But the longer the delay is, the longer the entire page will take to typeset, and the shorter the chunk is, the more often the page will have to reflow, and that can make the entire process longer in the end.  So you need to balance these factors to allow some responsiveness without taking forever to process the page.

The EqnChunkFactor is used to increase the EqnChunk after each chunk is typeset and displayed.  The idea is to have EqnChunk be small at he beginning, so that the math at the top of the page is typeset and displayed quickly, and then grow the size of EqnChunk so that longer pages will use larger and larger chunks (so that the delay doesn't add up to a huge delay with lots of page reflows).

Since you have pre-rendered equations, you can probably accept longer delays and smaller chunks in order to gain responsiveness.  So you might try (just off the top of my head)

MathJax.Hub.Config({
  CommonHTML: {
    EqnChunk: 10,
    EqnChunkFactor: 1,
    EqnChunkDelay: 200
  }
});

to give 1/5 second delay every 10 equations and see how that works out.  Let us know if you come up with values that seem to work well for your situation, as it might be useful for others.

Davide

Dupéron Georges

unread,
Jul 24, 2017, 7:36:05 PM7/24/17
to mathja...@googlegroups.com
Thanks a lot for the detailed reply David!

2017-07-24 20:51 GMT+02:00 Davide Cervone <dpv...@gmail.com>:
What I suspect is happening is that when an @font-face directive for a web font is inserted into the page, the browser needs to look through the CSS of all elements to see if any of them need to be updated based on the newly requested font, and if so, the page needs to be played out again (by inserting either blank space or a default font until the web font is loaded).

Hm, I would have expected browsers to handle this more efficiently, especially when it has no effect. They have some sort of optimised lookup data structure to detect CSS changes (e.g. adding a new CSS directive which does not affect any on-page elements is instantaneous), but apparently do not have a similar one for fonts.
 
If I understand correctly, your mathjax-node output is HTML (rather than SVG), so your previews are using the CommonHTML output.  For that, you should be including the CSS that you obtained from mathjax-node so that the will format properly.  That will include the @font-face CSS needed for CommonHTML output, and since it will be in the page already at the outset, if you use the CommonHTML output for the in-page re-rendering (rather than the HTML-CSS output that your page is configured to use), there is no need for new @font-face CSS to be inserted into the page, and you can avoid the changes that are causing the slow page reflows entirely.

I noticed the @font-face had different names for the pre-rendered and live versions. The HTML-CSS vs CommonHTML difference explains that, thanks! I'll try your code to use MathML while keeping the styles injected by MathJax-node.

There are several configuration parameters that control how responsive MathJax is while it is rendering the page, and allow you to trade off overall rendering speed for UI responsiveness.  For pages with large numbers of equations, you might need to adjust these for better scrolling during rendering, at the cost of taking longer to render all the equations.  In your case, since you have pre-rendered equations, that should not be a problem.

I had already tried playing with these, but unfortunately even with EqnChunk:1, EqnChunkFactor:1 and a large value for EqnChunkDelay, MathJax was still executing in 200ms chunks, which still makes the browser somewhat painful to interact with. My (untested) hypothesis is that typesetting a single equation causes a reflow, and given the large page it takes a while for the browser to perform that reflow (and for some reason, giving a fixed size to a separate container around each equation does not prevent these reflows).
 
Since Javascript is single-threaded, if a javascript program is running, the user interface is blocked.  So for something like MathJax that can run for a long time, in order to allow the user to do things like scroll the page or even to let the browser to update the typeset equations on the page, MathJax just voluntarily give up the CPU by pausing for a bit.  These parameters control how often MathJax pauses, and for how long.

Yes, that's a pity and a makes development of JS apps which do not freeze the browser difficult. it would make sense for the to allow scrolling around the page during the execution of JavaScript code: there's no reason why the JavaScript thread and the browser's UI thread should be the same. Most events (like clicks and MouseOver) can only be handled after the end of the currently executed script, of course, but simply displaying different parts of the page generally can be done without waiting for JavaScript to handle the event (the exception being "infininte scoll" pages and similar techniques). But I doubt something like this is in the future of JavaScript, especially since we have multithreading with web workers (which are not really suited to MathJax, as DOM manipulations are impossible within web workers).

Thanks again for your detailed answer,
Georges Dupéron

Davide Cervone

unread,
Jul 26, 2017, 3:50:26 PM7/26/17
to mathja...@googlegroups.com
What I suspect is happening is that when an @font-face directive for a web font is inserted into the page, the browser needs to look through the CSS of all elements to see if any of them need to be updated based on the newly requested font, and if so, the page needs to be played out again (by inserting either blank space or a default font until the web font is loaded).

Hm, I would have expected browsers to handle this more efficiently, especially when it has no effect.

Me, too, but that seems to be the source of the issue.  Note that it occurs even with CommonHTML, which does NOT wait for fonts to load, as HTML-CSS does.  The delay comes purely from the CSS insertion.

There are several configuration parameters that control how responsive MathJax is while it is rendering the page, and allow you to trade off overall rendering speed for UI responsiveness.  For pages with large numbers of equations, you might need to adjust these for better scrolling during rendering, at the cost of taking longer to render all the equations.  In your case, since you have pre-rendered equations, that should not be a problem.

I had already tried playing with these, but unfortunately even with EqnChunk:1, EqnChunkFactor:1 and a large value for EqnChunkDelay, MathJax was still executing in 200ms chunks, which still makes the browser somewhat painful to interact with. My (untested) hypothesis is that typesetting a single equation causes a reflow, and given the large page it takes a while for the browser to perform that reflow (and for some reason, giving a fixed size to a separate container around each equation does not prevent these reflows).

Yes, when an equation shows up, you will get a reflow (having anything new display will cause a reflow), and that means the offset positions of all elements need to be recomputed.  Because of the large number of elements, that is expensive.  So EqnChunk:1 is probably NOT the optimal choice.  That is why I recommended EqnChunk:10.  You might even want a larger value.

Davide
Reply all
Reply to author
Forward
0 new messages