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

Docs on Performance Considerations #878

Open
paulirish opened this issue Mar 25, 2013 · 32 comments
Open

Docs on Performance Considerations #878

paulirish opened this issue Mar 25, 2013 · 32 comments

Comments

@paulirish
Copy link
Member

paulirish commented Mar 25, 2013

Update: Read this comment: #878 (comment)




Modernizr needs to be placed in the <head> for two reasons: 

  1. html5shiv needs to be there for oldIE. 
    • only relevant for people supporting oldIE or including html5shiv.
  2. avoiding the FOUC when using modernizr-placed classes for feature-conditional styling
    • only relevant for people using them. some people don't have a dependency here.

Let's just document this so people know what's up.

  • You can throw an async attribute and/or place it at the bottom if neither of these matter to you.
  • Runtime overhead is tiny (run the CPU profiler and look for anything in Modernizr.js)
  • Network overhead may be large enough for you to consider defering the script load to later.

Can someone take a crack at drafting this section for the docs?

@igrigorik
Copy link

(1) How crazy would it be to move the conditional html5shiv logic into the parent doc instead of keeping it inside of modernizer? Or.. we should make it clear in the docs that if that's the only thing you need modernizr for, then inlining the conditional check into the doc is a much better way (performance wise) to integrate it.

@paulirish when should you not have async on the script? Could we make async the default recommendation?

I wouldn't discount runtime and network overhead.. I've registered tens of milliseconds for modernizr in the past (on my macbook pro), and the fact that it usually lives in the head places it right in the critical rendering path.

@aFarkas
Copy link
Member

aFarkas commented Apr 1, 2013

@igrigorik

Modernizr has a lot of tests, which might cause repaints (especially addClass and injectElementsWithStyle). If you put modernizr in the head, the tests are always working extremely fast. If you put Modernizr at the bottom or loading it async, it depends on the complexity of your HTML and CSS, how fast Modernizr is executed. (especially CSS including non matching selectors).

Here is a simple example, which measures the execution time of Modernizr and outputs it in the console:
http://corrupt-system.de/modernizr-perf/docs/

Due to the fact, that I'm almost using a CMS (don't know how large the HTML will be) and my CSS is always over 50kb (without compression), I never put it bottom/load it async.

@igrigorik
Copy link

@aFarkas this is very interesting. I'm not sure I fully grok the results or implications yet, but some test data:

Looking at the debug output: head: 21.000ms, async: 297.000ms, bottom: 471.000ms

First of all, the variability here is massive.. so I wouldn't take those numbers at face value, but there is definitely something interesting going on here. Further, the real interesting bit is that the SpeedIndex / time to first paint, doesn't seem to vary all that much. But this deserves a much closer and in-depth analysis..

I'm not all that familiar with inner workings of addClass and injectElementsWithStyle. From what I can tell addClass adds names of supported features into root tag, and that's what you're referring to in terms of reflows / repaints? What's the logic behind injectElementsWithStyle?

@ryanseddon
Copy link
Member

@igrigorik not necessarily injectElementsWithStyles itself but lots of tests that use it can cause reflows as most check offsetHeight on the injected element to see if certain styles have been applied.

@aFarkas
Copy link
Member

aFarkas commented Apr 2, 2013

@igrigorik

About the the repaints/reflows. Like @ryanseddon already said, addClass and injectElementWithStyles doesn't always create reflows/repaints. addClass can create a repaint or reflow, if you are using some of those classes in your CSS. If you are embedding Modernizr into your head it will never create a reflow/repaint because the browser hasn't yet rendered any layout things (modern browsers only DOM).

But even if you are not using those classes (my example do not use any of them) the CSS selector engine has to check wether the change on the root element has caused some style changes and changing the root element can cause style changes on all elements. Same goes to injectElementsWithStyles even if there is neither a reflow or a reapint. As soon as you include it at the bottom or async, the CSS engine has to look wether the injected element matches any selector in your CSS. This can be expensive.

The intersting part about the tests above. If you change to another page with different HTML and CSS the async and bottom tests performance will change, but the head should be stable.

@igrigorik
Copy link

@aFarkas @ryanseddon (pardon my naivete...) ... couldn't the cost of traversing a deep HTML tree be offset by running the test inside an injected iframe, or similar? That may not solve the entire problem, but potentially address at least a part of it?

@stucox
Copy link
Member

stucox commented Apr 8, 2013

Running it async / bottom of the page could also cause extra requests (as well as a FOUC) when using Modernizr's classes if not used carefully:

.thing {
    /* Will match on first paint and request file because Modernizr hasn't run yet */
    background-image: url(image1.png);
}
.feature .thing {
    background-image: url(image2.png);
}

Obviously changing the first selector to .no-feature .thing {...} would solve this, but it might be worth pointing out.

@stucox
Copy link
Member

stucox commented Apr 8, 2013

Another example of the above:

.no-js .thing {
    background-image: url(image1.png);
}
.js .thing {
    background-image: url(image2.png);
}

I don't know how to avoid 2 requests firing here when Modernizr loads async. Currently we switch no-js to js classes even when the user doesn't select the classes build option.

Users making use of the change in #808 would be affected by this kind of problem too.

@igrigorik
Copy link

@stucox interesting, thanks for the example.. this is indeed, a gotcha.

I guess in a nutshell, if your code / markup is conditional of modernizr classes, then deferring the calculation + injection of those classes will be more costly, where more is highly variable on the site - the more it depends on these classes, and the more complex the page, the worse the end result.

Hmm! This, of course, doesn't help with our goal of eliminating external JS to avoid the extra latency on mobile...
www.igvita.com/slides/2013/breaking-1s-mobile-barrier.pdf

@stucox
Copy link
Member

stucox commented Apr 11, 2013

Probably worth referencing @SlexAxton's article here — Deploying JavaScript Applications.

The scout file technique relies on Modernizr being inlined in the initial page load (and hence critical rendering path); but you then try to minimise requests after that. Avoids these styling race conditions at least.

A v3.0 build with only a few detects, with the setClasses & load options, can be < 3kB once minified and gzipped, so in these cases inlining makes more sense than async'ing, to me.

@igrigorik
Copy link

@stucox thanks, that was a great read. In a nutshell: inline fonts and images into CSS, inline CSS into JS, and then use a loader to figure out which JS bundle to pull down. :-)

I guess to come around full circle on this whole issue, things we discussed:

(1) Loading Modernizr async could have some interesting negative side-effects: double downloads, expensive layout / reflows, and FOUC side effects amongst other gotchas. As such, perhaps some applications could defer its evaluation, but it really depends on which features they rely on, and the complexity of their pages..

A somewhat related question to consider is: are there sites that have it included that don't actually use it? I have reasons to believe that this is indeed the case.. as I've encountered dozens of sites in this category.

(2) The good news is, the simplest minified+gzip'ed version of modernizr is 3.4KB, which means it could be inlined into the page and leaves us with ~11KB of budget for the rest of the content (to avoid extra RTT's). Of course, the more features you add in your modernizr build, the larger the size.. Hence: make sure you need the features you include in your modernizr build.

tl;dr.. Check if you need it, if you need it, check that you need the included modules, and finally, inline it.

Anything I'm forgetting?

stucox referenced this issue in stucox/html5-boilerplate Apr 24, 2013
@sp90
Copy link

sp90 commented Oct 7, 2013

i stumbled upon these:
OffsetTop, OffsetLeft, OffsetHeight etc. are forcing synchronous layout this is fixed if you use
documentOffsetTop, documentOffsetLeft etc.

This performance statistic made by jsperf_See link at the bottom_ shows the Offset vs documentOffset that in most of the newer browsers it have a better performance so i tested it out my self inside modernizr and i saw average performance improvement which i wanted to share

(im not good with github and i wanted to make a "fork" of this but contact me on simonpetersen3AThotmailDOTcoDOTuk for the code i changed)

My benchmark:
Evaluate Script - Details:

Total duration

offset:
113ms
148ms
117ms
103ms
134ms
123ms
115ms

documentOffset:
32ms
110ms
111ms
103ms
101ms
114ms
69ms

next place to improve performance is .appendChild but havn't really dogged into it yet tough

*http://jsperf.com/documentoffsettop-vs-offset-top

@stucox
Copy link
Member

stucox commented Dec 23, 2013

Could we improve the repaint issue by using requestAnimationFrame() where supported?

/cc @aFarkas

@hexalys
Copy link

hexalys commented Feb 15, 2014

In terms of performance, I am really confused as to why Modernizr test all cssomPrefixes every time? (If i am reading correctly). It's extremely easy to reliably detect the engine first, then only test the prefix for that engine and the non prefixed version. Testing them all is an unnecessary waste of resources.

@stucox
Copy link
Member

stucox commented Feb 15, 2014

@hexalys Some browsers support multiple prefixes: older versions of Opera, for instance. That said, we could probably still make some efficiency savings, if only by determining the ‘most likely’ prefix then checking that first each time we do a testAllProps().

@hexalys
Copy link

hexalys commented Feb 16, 2014

@stucox I am aware of Presto support for webkit prefixes, though I don't get the usefulness of that. It was only making things worse IMO. Is there an actual compelling case for testing Webkit prefixes for Presto Opera? My sense of correctness as a developer to get Presto support is to add the -o- prefix in my css, or else forget about it.

@stucox
Copy link
Member

stucox commented Feb 16, 2014

I had it in my head there were properties for which Presto had only implemented the -webkit- prefix – but on reflection that’s probably wrong (and there isn’t any evidence for it on caniuse.com).

I’ve raised #1223 to discuss this further.

@ryanseddon
Copy link
Member

Here's a resource where it says opera will use -webkit-. I don't know how relevant this is anymore with the move to Blink.

@paulirish
Copy link
Member Author

(draft, updated Feb 2016)

Performance Guidelines for Using Modernizr

Historically, the recommendation was for Modernizr to be placed in the head. There are two reasons:

  1. the html5shiv which is required for using HTML5 elements in <= IE8 must be executed before IE finds one of those tags in the HTML.
  2. Any styling hooks you have based on Modernizr's classes may cause a FOUC if Modernizr is executed lazily

So to be safe, the recommendation was to keep Modernizr as a request in the head. In this day and age, it's irresponsible to continue to recommend that.


Let's walk through a few situations and what action you need to take:

You are serving HTML5 elements in IE8

  • This includes any of these elements but most commonly it means <header>, <footer>, <aside>.
  • You should inline the HTML5Shiv in an inline script tag at the top of the document. This should not need to be a network request.
  • UA-sniffing IE8 to conditionally include this script would be a reasonable and good choice. (Yes, we just said that)
  • If you are using HTML5 elements but you do not support IE6-8, then you don't need the html5shiv at all.

You are using styling hooks of Modernizr like .csstransitions

  • First you need to evaluate if the styling hooks affect the critical path content. If you didn't run those detects would the page be visibly broken as it was loading in?
  • For example, If you're using .flexbox in your topnav, then probably you need it.
  • However, If you're using something like .csstransitions, or anything that isn't initially visible, it's probably not necessary to run Modernizr right up front.
  • Based on this, you can choose to defer executing Modernizr. If you can defer it, please do. Don't put it in the head and put it down in the bottom of the body like the rest.

You using the JS API of Modernizr like Modernizr.localstorage

  • You likely do not need to run these up in the head. It can be executed right before your application code. Defer it.

You're not doing any of the above things.

  • I have no idea why you're using Modernizr. I guess you shouldn't?

So at this point you should know if you need Modernizr in the <head> or not.

If you do need it in the head, you should inline it. Yeah, an inline script. (Probably best if you let a build script do that for you. (mod_pagespeed will automatically do this)). An inline script in the head will help get the page content to the user much faster than stalling the content behind the modernizr network request.

If you're deferring, then you have a few options. Either use script[defer], move the script tag, or generally include the module later in the build.


Summary:

  • If you need html5shiv, split it out from modernizr and conditionally include it for IE8 (better as a conditionally included inline script)
  • Determine if your use of Modernizr requires it to run in the <head>. If it doesn't, defer loading it till the bottom of <body>
  • If you do need it in the head, make the build as small as possible and drop it into an inline script.
  • Do not continue to use a <script src=modernizr.js> from within the <head>

@paulirish
Copy link
Member Author

⬆️ My attempt so far, edits and suggestions welcome.

@aFarkas
Copy link
Member

aFarkas commented Apr 26, 2014

@paulirish
While this includes all facts. The beginning says:

So to be safe, the recommendation was to keep Modernizr as a request in the head. In this day and age, it's irresponsible to continue to recommend that.

I don't think this is right. Modernizr is a tiny and cacheable asset. The network performance hit will occure only once and will be "small".

But the performance decrease for the runtime will be there with each website rendering. Therefore I still think the default suggestion should be "include Modernizr in the head of your document*.

If you look into the test made with full Modernizr 2.6 (without third party detects) one year ago, you will see that including Modernizr in the head already wins on first impression. Not to mention the cached impression. This was the runtime performance of Modernizr in Chrome (using this site as a testing enviroment):
head: 21.000ms, async: 297.000ms, bottom: 471.000ms

I think the recommendations should be a simple no brainer:

  1. If you use HTML5shiv, you must include Modernizr in the head.
  2. If you use the addClass option, you should include Modernizr in the head.
  3. If you neither use HTML5shiv nor addClass option (and no tests manipulating the DOM inside of the document), you should include Modernizr at the bottom (depends on specific Modernizr tests and your DOM)

Developers, who further want to omptimize the network performance should:
a) make Modernizr as small as possible to be able to inline it
b) run their own tests with all targeted browsers on their concrete website, because the recommendations 2. and 3. really depend on the site they are developing.

About making Modernizr smaller:

If someone includes all JS at bottom, most code of html5shiv is also only needed there. In this case only the style (for performance!) and the good old createElement part needs to be in the head. Perhaps I would maintain also a html5shiv-minimal.js that should be included in the head and if this is used one of the other shivs can be used async. What do you think about this?

I made a testcase for this, which you find here:
https://gist.github.com/aFarkas/11315175
Size is about 200/300 bytes vs 1.2kb/1.9kb.

@aFarkas
Copy link
Member

aFarkas commented Apr 26, 2014

Just made some test with different websites. The performance decrease isn't that heavy like I tested on the site above.

Here are the testresults:

modernizr.com:

Firefox:
head: 6ms
bottom: 13ms

Chrome:
head: 6ms
bottom: 14ms

IE9:
head: 6ms
bottom: 9ms

en.wikipedia.org/wiki/Main_Page

Firefox:
head: 8ms
bottom: 31ms

Chrome:
head: 7ms
bottom: 35ms

www.youtube.com

Firefox:
head: 8ms
bottom: 100ms

Chrome:
could not test ssl

www.amazon.com/:

Firefox:
head: 8ms
bottom: 43ms

Chrome:
head: 7ms
bottom: 70ms

IE9:
head: 7ms
bottom: 70ms

@ryanseddon
Copy link
Member

I'm not sure how true this is:

One rendering performance concern. Many of Modernizr's tests (especially the CSS ones) cause reflows AKA layouts. A series of recalc-styles,layout,paints may be triggered by Modernizr. If you're running this at the bottom of body, the page has a very complex DOM and Style setup already, and therefore these operations will take longer. When they run in the head, they are fairly fast. It's hard to make a rule about which is slower: In the it'll add execution cost that blocks rendering, at the bottom of it'll add some extra jank while the page's JS is executing. Measure it if you want, but deferred to bottom of body is probably better/faster.

Of the CSS features it's on an element that never gets injected into the DOM. Of the ones we do inject it's at the bottom in a div which wouldn't trigger massive style recalcs and those usually have position: absolute applied so even reflow should be minimal. Based on @aFarkas numbers execution costs seem to be pretty minimal.

Inlining in the head if it must be there is a good recommendation.

@aFarkas
Copy link
Member

aFarkas commented Apr 27, 2014

@ryanseddon
Even if there is no reflow/repaint as soon as you insert an element, the selector engine has to resolve all selectors for the given element. In the past I made some tests with a bunch of complex selectors, which do not match any element. Removing them had a huge positive impact on the performance for any DOM-manipulation.

While those numbers show that the performance decrease isn't in total that bad, I still have my problems with the recommendation. I think that an increase of network performance by about 100ms only on first impression doesn't outweight a decrease in runtime performance by about 20ms. The reason for this is a) runtime will be always slowed down with each page view, while network performance is only for the uncached page view improved, b) if there is an additional script request in the head, the browser might block rendering the page, but the browser isn't blocked, so it can parallelize a lot of tasks in the background. On the contrary while script is executed the browser is fully blocked and can't do anything and c) the decrease of the runtime performance isn't stable. We can not say it's always about 3x slower. The numbers do say it's between 1.5x - 12x slower depending on different circumstances (especially the amount and complexity of the used css selectors).

@aFarkas
Copy link
Member

aFarkas commented Apr 27, 2014

This said, if someone really wants to squeeze out page performance, we endup with just one recommendation:
a) make it small (html5shiv-minimal ???)
b) inline it in the head

@paulirish
Copy link
Member Author

Do all these efficiencies about putting modernizr at th bottom of a page also apply when Loading resources using Modernizr.load?

It's unrelated. There is nothing about Modernizr.load that requires Modernizr to be in the head.

So yeah they do apply. :)

@rupl
Copy link
Contributor

rupl commented Sep 23, 2014

The page won't make the async requests until they're found. So I'm thinking that it's probably beneficial to put Modernizr.load requests before other JS resources that block. I don't have any data to prove this, just intuition.

@igrigorik
Copy link

@paulirish html5shiv aside, any reason why we can't use script[async+defer] instead? In which case, putting it at the top of the head is not a problem... and actually helps the browser to discover it sooner.

@lin7sh
Copy link

lin7sh commented Oct 23, 2015

The Guide is so awesome

mhl added a commit to mysociety/pombola that referenced this issue Jun 2, 2016
Modernizr is now only used for the HTML5shiv, but included lots more
that now isn't needed (e.g. yepnope) and was on an old version anyway.
So, it makes sense to update the version of Modernizr and create a
new minimal build. (The custom Modernizr build in the current version
outputs a URL to reproduce the build, so it'll be easier to update
now as well.

There was some of our own Javascript used for SVG feature detection,
and since this can be done by Modernizr, I've used that instead.

Fixes #1774

Incidentally, there is a strong argument that we shouldn't put
Modernizr in the <head>:

  Modernizr/Modernizr#878 (comment)

... and inline the HTML5 Shiv (including html5shiv-printshiv.js).

I'll create an issue about considering that.
mhl added a commit to mysociety/pombola that referenced this issue Jun 2, 2016
Modernizr is now only used for the HTML5shiv, but included lots more
that now isn't needed (e.g. yepnope) and was on an old version anyway.
So, it makes sense to update the version of Modernizr and create a
new minimal build. (The custom Modernizr build in the current version
outputs a URL to reproduce the build, so it'll be easier to update
now as well.

There was some of our own Javascript used for SVG feature detection,
and since this can be done by Modernizr, I've used that instead.

Fixes #1774

Incidentally, there is a strong argument that we shouldn't put
Modernizr in the <head>:

  Modernizr/Modernizr#878 (comment)

... and inline the HTML5 Shiv (including html5shiv-printshiv.js).

I'll create an issue about considering that.
mhl added a commit to mysociety/pombola that referenced this issue Jun 2, 2016
Modernizr is now only used for the HTML5shiv, but included lots more
that now isn't needed (e.g. yepnope) and was on an old version anyway.
So, it makes sense to update the version of Modernizr and create a
new minimal build. (The custom Modernizr build in the current version
outputs a URL to reproduce the build, so it'll be easier to update
now as well.

There was some of our own Javascript used for SVG feature detection,
and since this can be done by Modernizr, I've used that instead.

Fixes #1774

Incidentally, there is a strong argument that we shouldn't put
Modernizr in the <head>:

  Modernizr/Modernizr#878 (comment)

... and inline the HTML5 Shiv (including html5shiv-printshiv.js).

I'll create an issue about considering that.
mhl added a commit to mysociety/pombola that referenced this issue Jun 2, 2016
Modernizr is now only used for the HTML5shiv, but included lots more
that now isn't needed (e.g. yepnope) and was on an old version anyway.
So, it makes sense to update the version of Modernizr and create a
new minimal build. (The custom Modernizr build in the current version
outputs a URL to reproduce the build, so it'll be easier to update
now as well.

There was some of our own Javascript used for SVG feature detection,
and since this can be done by Modernizr, I've used that instead.

Fixes #1774

Incidentally, there is a strong argument that we shouldn't put
Modernizr in the <head>:

  Modernizr/Modernizr#878 (comment)

... and inline the HTML5 Shiv (including html5shiv-printshiv.js).

I'll create an issue about considering that.
mhl added a commit to mysociety/pombola that referenced this issue Jun 3, 2016
Modernizr is now only used for the HTML5shiv, but included lots more
that now isn't needed (e.g. yepnope) and was on an old version anyway.
So, it makes sense to update the version of Modernizr and create a
new minimal build. (The custom Modernizr build in the current version
outputs a URL to reproduce the build, so it'll be easier to update
now as well.

There was some of our own Javascript used for SVG feature detection,
and since this can be done by Modernizr, I've used that instead.

Fixes #1774

Incidentally, there is a strong argument that we shouldn't put
Modernizr in the <head>:

  Modernizr/Modernizr#878 (comment)

... and inline the HTML5 Shiv (including html5shiv-printshiv.js).

I'll create an issue about considering that.
mhl added a commit to mysociety/pombola that referenced this issue Jun 3, 2016
Modernizr is now only used for the HTML5shiv, but included lots more
that now isn't needed (e.g. yepnope) and was on an old version anyway.
So, it makes sense to update the version of Modernizr and create a
new minimal build. (The custom Modernizr build in the current version
outputs a URL to reproduce the build, so it'll be easier to update
now as well.

There was some of our own Javascript used for SVG feature detection,
and since this can be done by Modernizr, I've used that instead.

Fixes #1774

Incidentally, there is a strong argument that we shouldn't put
Modernizr in the <head>:

  Modernizr/Modernizr#878 (comment)

... and inline the HTML5 Shiv (including html5shiv-printshiv.js).

I'll create an issue about considering that.
@onmarketing
Copy link

Hi,

Via Google Page Speed Test:

Your page has 1 blocking script resources and 3 blocking CSS resources. This causes a delay in rendering your page.
None of the above-the-fold content on your page could be rendered without waiting for the following resources to load. Try to defer or asynchronously load blocking resources, or inline the critical portions of those resources directly in the HTML.
Remove render-blocking JavaScript:
http://www.on-preview.co.uk/…pts/modernizr-3.5.0-respond-1.4.2.min.js

Inside head - <script src="assets/scripts/modernizr-3.5.0-respond-1.4.2.min.js"></script>

For faster page loading, which do I use async or defer?

Thanks,

Shaun.

toddmilliken pushed a commit to bu-ist/responsive-framework that referenced this issue Jul 24, 2018
… Modernizr to be loaded in the footer.

Enqueueing Modernizr before `responsive-scripts` allows `modernizr` to be added as a dependency. This ensures Modernizr is always loaded first, incase any theme or framework JavaScript depends on Modernizr functionality.

Loading Modernizr in the footer is generally ok, but there are situations that would experience issues (see Modernizr/Modernizr#878 (comment)). For that reason, we will leave Modernizr in the header by default. But if a theme needs to, for some reason, they can move Modernizr to the footer using `r_modernizr_in_footer`.
toddmilliken pushed a commit to bu-ist/responsive-framework that referenced this issue Oct 25, 2018
… Modernizr to be loaded in the footer.

Enqueueing Modernizr before `responsive-scripts` allows `modernizr` to be added as a dependency. This ensures Modernizr is always loaded first, incase any theme or framework JavaScript depends on Modernizr functionality.

Loading Modernizr in the footer is generally ok, but there are situations that would experience issues (see Modernizr/Modernizr#878 (comment)). For that reason, we will leave Modernizr in the header by default. But if a theme needs to, for some reason, they can move Modernizr to the footer using `r_modernizr_in_footer`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests