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

Try to remove or reduce the FOUC problem #39

Open
ausi opened this issue May 24, 2017 · 10 comments
Open

Try to remove or reduce the FOUC problem #39

ausi opened this issue May 24, 2017 · 10 comments
Assignees

Comments

@ausi
Copy link
Owner

ausi commented May 24, 2017

I think if the script loads fast enough and the page ist not too long it should be possible to avoid a flash of unstyled content.

We would need an event that fires directly before the first render, maybe a requestAnimationFrame? In the initial rendering case we should also try to run the process synchronously so that we don’t miss the first render.

See also https://twitter.com/etportis/status/867023541614460928

/cc @eeeps

@ausi ausi self-assigned this May 24, 2017
@eeeps
Copy link

eeeps commented Jun 2, 2017

Relevant thread with the Shopify team who implemented, then abandoned, JS-based container queries on their admin backend:

https://twitter.com/_lemonmade/status/870461985334362112

Here's what I think they implemented:

https://github.com/lemonmade/container-queries

I can't tell by looking at the code myself; any reason that’s FOUC-ier or less FOUC-y than cq-prollyfill?

@eeeps
Copy link

eeeps commented Jun 2, 2017

Regarding requestAnimationFrame — does it matter that it happens just before layout (rather than between layout and paint, ala ResizeObserver)?

Rendering Pipeline

@ausi
Copy link
Owner Author

ausi commented Jun 2, 2017

From reading the code I don’t see a reason for it to be FOUC-ier than cq-prolyfill. But it depends on how you load the JavaScript and when you call ContainerQuery.create().

The big problem I see with it is synchronous layouts and layout thrashing. I looks like for every node that has a container query attached a synchronous layout is performed. This can make it really slow. cq-prolyfill is optimized to do as few synchronous layouts as possible. I wrote about how this is done in WICG/container-queries#3 (comment)

requestAnimationFrame running before layout is not a problem I think, the important part is that is should run before the very first render/paint so that we can avoid a FOUC. Once the DOM of the page is fully loaded (domready) we can remove the requestAnimationFrame callbacks and switch to ResizeObserver.

@eeeps
Copy link

eeeps commented Jun 2, 2017

I thought you were looking to use requestAnimationFrame because ResizeObserver isn't really supported anywhere yet. If you're only hoping to use it so that your callback fires before first paint – why can't you use ResizeObserver? I think RO callbacks also fire before first paint.

Example: https://ericportis.com/etc/ResizeObserver-runs-before-first-paint.html
Result: https://ericportis.com/etc/ResizeObserver-runs-before-first-paint-screenshot.png
(this is a slightly-fleshed-out test of https://twitter.com/etportis/status/870673993207955457)

(PS the dirty nodes / sync layout stuff is still fuzzy in my head, need to dive into your code to really understand...)

@ausi
Copy link
Owner Author

ausi commented Jun 3, 2017

I didn’t know that MutationObserver works while the HTML document is still downloading, I thought it only observes dynamically injected nodes. Because of that, cq-prolyfill is already FOUC-free. You can test that with this example:

<?php function delay() { echo str_repeat(' ', 10 * 1024); ob_flush(); flush(); sleep(1); } ?>
<!DOCTYPE html>
<head>
	<meta charset="utf-8">
	<script> window.cqConfig = { postcss: true }; </script>
	<style> div.\:container\(width\>100px\) { color: red } </style>
	<script src="https://cdn.rawgit.com/ausi/cq-prolyfill/v0.3.3/cq-prolyfill.js"></script>
</head>
<body>
	<div id="div1">div1</div><?php delay(); ?>
	<div id="div2">div2</div><?php delay(); ?>
	<div id="div3">div3</div><?php delay(); ?>
</body>

All divs should be in red color as soon as they show up.

@ausi
Copy link
Owner Author

ausi commented Jun 3, 2017

This is how that process for one of those divs looks like in the browser:

timeline

  1. Receive Data
  2. Parse HTML
  3. Run Microtasks (cq-prolyfill jumps in)
    1. Recalculate Style and Layout to check if the query matches
    2. Apply the container query CSS class to the div
  4. Recalculate Style
  5. Update Layer Tree
  6. Paint
  7. Composite Layers
  8. Update Layer Tree

@eeeps
Copy link

eeeps commented Jun 3, 2017

So, when executed in the <head>, cq-prollyfill getComputedStyles (and therefore computes layout) once (or, if a cq matches, possibly more than once) for every :container()’d node?

Questions that come to mind (which I should be able to get some time to test/try to answer myself on Monday)...

  1. Does it therefore exhibit the scary performance characteristics that browser folks have foretold, e.g. https://twitter.com/tabatkins/status/866828359308726275 ?
  2. Does this mean that DOM parsing is halted/blocked until CSS is loaded and parsed?

@eeeps
Copy link

eeeps commented Jun 3, 2017

Ran one little test, and yeah! In the head = FoUC free!

(http://ericportis.com/etc/fouc-finder/ vs http://ericportis.com/etc/fouc-finder/defer.html)

But—why does the FoUC-free version take 1.5x the time to paint a fully-queried-and-styled page (3.73s vs 2.46s), and is there any way to make it faster?

(http://ericportis.com/etc/fouc-finder/head.png vs http://ericportis.com/etc/fouc-finder/defer.png)

@ausi
Copy link
Owner Author

ausi commented Jun 4, 2017

In my tests (with 20x CPU slowdown) the difference was not that big, the sync version took about 1.8 seconds and the defer version 1.6 seconds. But still, the defer version seems to be faster.

I think the reason for that is, that in the sync version the browser has to wait for the javascript to download and cannot parse the HTML and build the first layout in parallel.

The best compromise might be to load it async instead of defer. This would make it FOUC-free if the script is already in the cache.

Another performance problem I saw was that cq-prolyfill gets executed 4 times: One directly when the script loads, one called by the mutation observer, one for DOMContentLoaded and one for the load event. If all these events happen at the same time it might be possible to skip some of the executions.

@ausi
Copy link
Owner Author

ausi commented Jun 8, 2017

I created issue #41 for the problem with multiple executions.

And regarding your questions:

Does it therefore exhibit the scary performance characteristics that browser folks have foretold, e.g. https://twitter.com/tabatkins/status/866828359308726275 ?

Sync resize is not a problem because browsers will only support ResizeObserver. Re-layout is expensive but cq-prolyfill is optimized to do as few re-layouts as possible, most of the time only one re-layout is performed, independent of the number of container query nodes. In the worst (very uncommon) case the number of re-layouts is equal to the nesting level of container query nodes, in your fouc-finder example this number would be five.

Does this mean that DOM parsing is halted/blocked until CSS is loaded and parsed?

Yes, but if we implement #41 this shouldn’t be a problem because for rendering the first frame the browser waits for the CSS anyways.

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

No branches or pull requests

2 participants