As part of the initially simply project of a copy button on a web page, I discovered a need to in-line styles if they are to be conserved when pasted into an email, and that there are no satisfactory out-of-the-box client-side JavaScript style in-lining libraries to be found. So I opt instead, for a native JavaScript solution.
But it turns out that it's not quite so zippy in-lining styles like that. I'm not sure any other method is faster (except for Chrome's native method which on a Select and Copy in-lines them as good as instantly).
A minor mystery, as we should be able to get performance as good as the browsers rendering (after all, the browser is interpreting the CSS on each element and working out where to place it and how to draw it, and our effort to simply work out a subset of that and inline it should by rights be as quick). But we don't seem to get native browser performance (one might suppose the native C or C++ browser implementation has an advantage of the interpreted Javascript implementation I guess).
Profiling it reveals no major culprits, it's all just milliseconds here, milliseconds there and it's not bad performance. On a fairly ordinary element that I'd like to copy (a table of tables) I have about 3,500 elements, and these have styles in-lined in about 1 second As a delay after pressing the copy button and having the copy available on clipboard that's not intolerable, it's quite acceptable, if not impressive.
But what if we want to copy a lot? Well on one of my largest datasets I'd ever likely copy, I have about 100,000 elements and these have styles in-lined in about 30 seconds. That is getting a little clumsy to say the least. It means that if you click the copy button and then paste somewhere, chances are it's not available yet it's still being in-lined and not on the clipboard yet!
What better reason to look into a few Javascript tricks to streamline this and hone our skills, to make that experience a bit slicker?
Key strategies for improving the UX on such large copies (that can take 30 seconds to prepare) follow:
Separation of Tasks
My first thought is to separate the tasks, of in-lining styles and of copying to the clipboard. If we in-line the styles when the page loads, then a copy is available instantly more or less to place on the clipboard when the copy button is clicked.
Alas if we do that in the javascript of the page we run into a couple of small problems:
- Javascript is infamously single-threaded and this freezes the UI for the 30 seconds it takes to inline those styles. Longer even if you're more ambitious than I am with your copying ... (i.e. copying an even larger HTML element)
- if the element in question is itself generated by Javascript (as mine are) or worse fetched via Javascript (AJAX), then the styles in-lined are not even true and complete because the element hasn't been fully rendered while the in-lining is running.
So ideally we want to do the in-lining after the page is fully rendered and complete - that is, schedule it for later.
Scheduling of Tasks
The most generic way to do that, to ensure it runs not only after the DOM is fully loaded but also after all the dynamically Javascript-rendered elements are all settled, we can schedule the in-lining to happen when the page's ready state is "complete". That's easily done by watching for changes in ready state using a custom handler attached to the Document: readystatechange event.
But ... even when it's running at the end of rendering, it can lock the UI up for those 30 seconds, quite a problem on an interactive page that just won't respond and appear locked while that's going on. And so ideally we want to free up the UI somehow while in-lining these styles.
Deferring to the UI
To free up Javascript to handle UI interactions while our script is running, is essential for good UX. That is our script must defer to the UI ...
There is in fact a fairly widely used and cited little JavaScript snippet that does just that:
function defer() {return new Promise(r => setTimeout(r, 0));}
await defer();
But I have nowhere found any lucid explanation of what is going on there how and why this works, and I'll explore that in the next post.
Top comments (0)