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

Question: long (~500ms TTFB) initial load time for images? #12047

Closed
baobabKoodaa opened this issue Feb 24, 2019 · 20 comments
Closed

Question: long (~500ms TTFB) initial load time for images? #12047

baobabKoodaa opened this issue Feb 24, 2019 · 20 comments
Labels
type: question or discussion Issue discussing or asking a question about Gatsby

Comments

@baobabKoodaa
Copy link
Contributor

baobabKoodaa commented Feb 24, 2019

Summary

My Gatsby site is hosted on Netlify. If I hard refresh a single image, Netlify CDN delivers it to me in about ~50ms. If I hard refresh a very light page with that same image, it takes ~500ms to deliver the image. What's causing this?

Most of the time is spent in "Waiting (TTFB)".

If I disable JavaScript, the image loads in ~100ms.

So, my guess is that the request for the image is sent, then the page is "hydrated" into React, and it's blocking the browser from receiving content. Is this correct? Can I do something to improve the image load time?

I was testing with this page.

@gatsbot gatsbot bot added the type: question or discussion Issue discussing or asking a question about Gatsby label Feb 24, 2019
@m-allanson
Copy link
Contributor

This is strange... I can replicate the behaviour you're seeing. Using Chrome's Network tab with caching disabled and javascript enabled I see similar results to yours.

However, if I try and dig into this more using the Performance tab on Chrome, I see the normal 'fast' behaviour.

screenshot 2019-02-25 at 09 56 39

I'm reading this as:

  • onload fires at around 280 ms
  • the browser spends ~(22 + 49)ms on hydration and layout updates,
  • the mailboxes2.webp image takes ~80ms to become available
  • the fade in animation starts playing at ~420ms

which is roughly consistent with your ~100ms no javascript load time.

So, my guess is that the request for the image is sent, then the page is "hydrated" into React, and it's blocking the browser from receiving content. Is this correct? Can I do something to improve the image load time?

I'd need to check this, but I think the image request shouldn't happen until after the page has been hydrated. Before then, there should only be the svg placeholder (and the <noscript> tag with the source images). Once the page has hydrated, it should make a request for the real image, and when that's ready it'll swap that out with the placeholder.

In short - I'm not sure what's going on here! I'd be interested to hear what anyone else thinks.

@polarathene
Copy link
Contributor

I'd need to check this, but I think the image request shouldn't happen until after the page has been hydrated.

Yes, the full image won't be available until the gatsby-image element ref(handleRef()) on the component class runs to toggle isVisible when the image is in viewport(lazy-loading). That then causes the component to render again and add the picture element which will load the full image.

AFAIK, TTFB in this case should only be affecting the request/response side, and thus shouldn't be different to noscript, once cached, then you get the fast TTFB.

The browser is waiting for the first byte of a response. TTFB stands for Time To First Byte. This timing includes 1 round trip of latency and the time the server took to prepare the response. - DevTools explanation link

You can check the TTFB and what's causing the bulk of that time with this tool

Another consideration could be caching for offline or via service-worker(I guess that's offline?), Gatsby blog can have cache disabled, but will still pull the images from a service-worker cache in the network. (Not the case with given link in this case though).


I tried the linked example, and can also replicate same results both of you have mentioned. Slightly worse with my connection/location, sometimes TTFB could be as large as 1-2s, but I would get the 500ms and with JS disabled <100ms. The difference between browser refresh or performance profiler tab refresh for the TTFB network tab results is really odd!(I wonder what causes that!)

Now to make it even more confusing :) Here's one of my own netlify projects for some testing. Rather than a single image, I have 24(so it varies based on viewport what will get loaded when JS is enabled). You'll notice that with JS disabled it's worse for TTFB. I assume it's due to all the requests being made at once, even incurs queues for me. Perhaps this is a factor? Running a build locally via nginx docker container, there isn't any noticeable difference(<1ms TTFBs), even at "Slow 3G" network speed and "6x throttle" for CPU which yielded 2s TTFBs for both. Performance monitor also showed no difference here. Might be something specific to Netlify env or network that differs from my local env. My local version isn't entirely the same at the moment, so I'd need to push it to Netlify to confirm for sure.

@gatsbot
Copy link

gatsbot bot commented Apr 2, 2019

Hiya!

This issue has gone quiet. Spooky quiet. 👻

We get a lot of issues, so we currently close issues after 30 days of inactivity. It’s been at least 20 days since the last update here.

If we missed this issue or if you want to keep it open, please reply here. You can also add the label "not stale" to keep this issue open!

Thanks for being a part of the Gatsby community! 💪💜

@gatsbot gatsbot bot added the stale? Issue that may be closed soon due to the original author not responding any more. label Apr 2, 2019
@baobabKoodaa baobabKoodaa added not stale and removed stale? Issue that may be closed soon due to the original author not responding any more. labels Apr 2, 2019
@itmayziii
Copy link

itmayziii commented May 3, 2019

I've encountered the same exact issue except I'm not on Netlify. Our CDN is generally very fast when serving images directly but as soon as they are on a page with Gatsby the images take 10x longer sometimes.

I've come to the conclusion it has something to do with Gatsby's preloading or javascript files. Here is a test with 3 core javascript files blacklisted and on a slow 3G connection speed.

http://webpagetest.org/result/190503_PG_524a4214f2fb4822da42ff7aa6614c73/3/details/#waterfall_view_step1
I had disabled a lot of the JS files and narrowed it down to
/app-8ce39ab1decbe8930edf.js
/2-ac7f8f8c9979c102477a.js
/component---src-templates-webpage-js-65f96a6773c795a477ad.js

Here is the same test without any files blacklisted
http://webpagetest.org/result/190503_1V_7ddee097d769dc65dd698956393f4787/2/details/#waterfall_view_step1

I'm not sure what about these JS files is the issue but they are making our site's TTFB performance take WAY longer than it should. I've been looking for a way to disable preloading in Gatsby but I haven't been successful finding anything. I found out how to disable pre-fetching but they are not the same thing.

I hope these tests help someone find the root cause.

@polarathene
Copy link
Contributor

polarathene commented May 4, 2019

Summary response (TL;DR)

It appears the issue is due to HTTP/2, not Gatsby. All requests are sent at the same time for that HTTP/2 socket to handle(transfer responses back). The responses are queued onto a stream back to the browser client, and TTFB is reported as those assets arrive.

Thus they're including the download time of all requests served prior to them(from the same HTTP/2 stream) A large portion of that which makes up the notable difference is about 200KB of JS(3 requests).

If you want to deliver the images ahead of that, they'll need to be served from a different HTTP/2 socket. As this dependency visualization shows, serving them from a different (sub-)domain should be sufficient.

Although as I point out at the end of my post, domain sharding probably has no benefit to your sites performance here, it'll likely just make the TTFB look the way you want, with no meaningful gains due to overhead of opening a new connection.


Original response

I've encountered the same exact issue except I'm not on Netlify. Our CDN is generally very fast when serving images directly but as soon as they are on a page with Gatsby the images take 10x longer sometimes.

TTFB is the time from the request to when the server responds with the first byte to the browser. So that's not really anything to do with Gatsby, but rather the latency over the network and any latency on the server end.

Something that might be playing into it is the amount/rate of requests at once.

Looking at the headers HTTP/2 is used, performance for multiple requests at once with this improves over HTTP/1, but many tests have shown it to degrade as the request count increases, so this may be impacting the outcome as well. There are 6 additional requests for Document Complete, and 11 additional requests for Fully Loaded.

We can also see that overall load time is only about 1 second longer(~10%+), which is fair considering the increase of requests and additional weight they add. What is noticeably different is about 4 seconds difference to visually complete(~50%+).

Inspecting request headers for the hidpi logo png image:

1st test link:

Request 5
...
Priority: LOWEST
HTTP/2 Stream: 9, weight 147, depends on 7, EXCLUSIVE

2nd test link:

Request 8
...
Priority: MEDIUM
HTTP/2 Stream: 15, weight 147, depends on 13, EXCLUSIVE

The image is being requested at a later point but also has a higher priority assigned to it. Perhaps it's due the gatsby-image component. You could try compare again with critical prop applied, this should avoid any transition and lazy-loading logic iirc. When the image component mounts, it will immediately add the intended image vs waiting until lazy-load indicates when to mount the actual image(not just the placeholder). This should behave more like the noscript version. It wouldn't seem like that'd be the cause of the difference as it's meant to be a performance optimization, would be interesting to know.

This article details an aspect of HTTP/2 regarding priorities:

This means that an asset can be requested very early in the page load, but won’t be served until higher priority asset requests have been processed. This is why it has a long TTFB.

So that's interesting. Looking at the 2nd test results request data at the bottom, the problem file you cited, along with a few others are requested earlier than the images, but are all fired off at roughly the same time along with the images. The priority values for all are MEDIUM, rather than associating LOW to the images like in the 1st test link.

What is probably happening here is, all requests are sent off at the same time. The server now queues all those requests up, and once that's ready, begins to stream them over the single HTTP/2 socket to the browser client. Due to the way HTTP/2 works, the assets with higher priority in the queue will be delivered first, and then afaik it'll depend upon dependencies of those requests for ordering.

Requests 2-4 on the 2nd test link take roughly ~1,100 ms to download, you can actually see the TTFB column correlates with the content download column. This is why the TTFB is appearing so high. That first byte is delayed until that request is responded to from that single socket streaming content to the client. If you use HTTP/1 then as long as the additional overhead of new requests is not too high, you'd see much lower TTFB, but that might not actually mean much of a difference.

As you continue to move down comparing the columns, you'll notice TTFB stops increasing instead of jumping another 700ms, why is that? Looking at the request header data for request 12:

Priority: LOWEST
Protocol: HTTP/2
HTTP/2 Stream: 23, weight 147, depends on 19, EXCLUSIVE

The prior request for a large 60KB image was:

Priority: MEDIUM
HTTP/2 Stream: 21, weight 220, depends on 15, EXCLUSIVE

It wasn't dependent on the previous request like others have been, instead it depended on stream 19 which was request 10, star.png.

Priority: LOWEST
HTTP/2 Stream: 19, weight 147, depends on 17, EXCLUSIVE

So I got that slightly mixed up, the dependency chain affects placement on the queue, with priority adjusting when that request is served respective to it's adjacent dependencies. The hidpi logo image had medium priority, with the other large image(request 11) also being medium, but all those between the two were lower priority.

Turns out webpagetest has a handy link under one of the charts to visualize the dependency list better! That better illustrates it :)

That shows the hidpi logo having larger mobile jpg dependent on it with a higher priority value than the chain of lower priority dependencies next to it. I'd have thought it would download before the lower priority chain, but for whatever reason, it didn't, nor was the lower priority dependency chain respected. Perhaps as the article stated:

Every browser also uses different values for priority. These values can also be effected by their dependancies, but for our purposes we will use the simplified version of the story. These priorities are hints, and the server can choose to ignore them, but if honoured it will send the assets in order of priority.

But that should clear up this issue. What you've witnessed is nothing to do with Gatsby, it's just the way HTTP/2 works, the TTFB isn't accurate here per request since they're all served over a single stream but all requested/queued up front.

That extra 4 seconds until "Visually Complete" is a valid a concern but appears to be due to the extra requests to other platforms, including Google Maps and the GraphQL staging-api subdomain(adds additional lookups, you can use this to reduce that TTFB via domain sharding), other than the initial JS payload difference, which seems to reflect accurately on time to interactive(more meaningful metric) offset difference.

Google Developer docs on HTTP/2 Stream Prioritization

Note: Stream dependencies and weights express a transport preference, not a requirement, and as such do not guarantee a particular processing or transmission order. That is, the client cannot force the server to process the stream in a particular order using stream prioritization. While this may seem counter-intuitive, it is in fact the desired behavior. We do not want to block the server from making progress on a lower priority resource if a higher priority resource is blocked.

Cloudflare demonstrates that TTFB is not an accurate/reliable metric:

The TTFB being reported is not the time of the first data byte of the page, but the first byte of the HTTP response. These are very different things because the response headers can be generated very quickly

KeyCDN on Domain Sharding

domain sharding has become irrelevant and is in most cases recommended to avoid it.

It's generally ill-advised to utilize domain sharding these days with HTTP/2, but may be required if you want another connection/socket open for the assets TTFB that you want to shorten due to their delay in the stream queue. Note this will incur some overhead to open up a new socket(DNS lookup, TLS handshake, etc), which from your test results looks to be around 1+ seconds to perform anyhow, so there may not be any gains there other than the TTFB looking shorter in the network graph, but not actually having arrived any faster.

@polarathene polarathene changed the title Question: long (500ms) initial load time for images? Question: long (~500ms TTFB) initial load time for images? May 4, 2019
@polarathene
Copy link
Contributor

polarathene commented May 4, 2019

@baobabKoodaa

So, my guess is that the request for the image is sent, then the page is "hydrated" into React, and it's blocking the browser from receiving content. Is this correct?

Yes. The total content to download when JS is not blocked is almost double the size in the referenced site you provided. HTTP/2 streams all requests from the same hostname over a single socket instead of multiple requests like HTTP/1.

The long TTFB is due to all requests being sent upfront at once, and reported once that particular asset has received it's first byte by the browser. They are in fact blocked by assets earlier in the queue of the stream.

Can I do something to improve the image load time?

Using the critical prop on gatsby-image might work(you don't want to do that to every image asset). It'll opt out of lazy-loading(as well as any transition effect) which won't require waiting on React to hydrate and mount the full images within the viewport / above the fold. The image should exist in the markup outside of the noscript tag when delivered in the initial HTML payload this way. However images have a lower priority than JS, so the browser may still delay the images with this prop over the JS, which are also in preload tags within the head.

You would otherwise need the server to prioritize the assets ahead of the JS payload in the stream. Preloading assets above the fold might work, or using Server Push(you'll lose any browser cache benefit) should place priority above anything else.

You can also try domain sharding, but the initial overhead probably removes any benefit(it will just render/report a shorter TTFB). It's not really worthwhile in majority of cases with HTTP/2 these days.

Going to close this issue as it's resolved.

@baobabKoodaa
Copy link
Contributor Author

baobabKoodaa commented May 4, 2019

Thanks for looking into this. I am actually using the critical prop for the main image on my home page, but the issue is still pretty bad. When I disable JS in my browser, the main image loads <100ms, whereas when I enable JS, it takes about 600ms to load. Link.

I know you said this is not a Gatsby issue, but I have to disagree. I hope I'm not too harsh, but I have to say, If I compare these two options for creating a straightforward static website:

  1. Use Gatsby
  2. Write a simple static web page generator

...choosing Gatsby means literally 6x load times for critical images.

I know you said it's a HTTP2/resource prioritization issue, but a simple website just shouldn't be bundled with so much JavaScript that loading critical assets is delayed by 500ms. One of the main selling points of Gatsby is that it's supposed to improve your web site performance for you. Here it's doing literally the opposite of that.

@itmayziii
Copy link

itmayziii commented May 4, 2019

@polarathene

Thank you for the detailed response back, I really appreciate that. What you said about HTTP2 making TTFB appearing higher because of other higher priority things loading makes a lot of sense. I was able to disable preloading on our site and it didn't have a noticeable impact on overall load time of the page. Even though the waterfall started looking better for us with faster TTFB images, the overall load time did not improve, which makes a lot of sense given your explanation.

For those curious here is the code we used to disable preloading on our site, this is not something we plan on keeping since it didn't improve the metrics we were looking to improve (mainly load time.)

In gatsby-ssr.js

exports.onPreRenderHTML = ({ getHeadComponents, replaceHeadComponents }) => {
  replaceHeadComponents(removePreload(getHeadComponents()))
}

function removePreload (components) {
  return components.filter((component) => component.type !== 'link' && component.props && component.props.rel !== 'preload')
}

@baobabKoodaa
I don't believe our issue lies with Gatsby and relies more on the prioritization of HTTP2 resources. You may want to try the code above to disable pre-loading if your really concerned about that image, but as @polarathene pointed out TTFB is not a reliable metric with HTTP2 because multiple things are loading at once and lower priority things will appear to take a long time.

This article might also help you https://developers.google.com/web/updates/2019/02/priority-hints

Also Gatsby is a little more than an static site generator, it's powered by React when the page hydrates and acts as an SPA when clicking through gatsby linked pages. This let's us developers write in the frontend framework we love but still have the benefit of a static site. Like everything in life there is tradeoffs to our decisions, one of the tradeoffs we make when using Gatsby is that will have a little more JS than other options because we chose to use React to write the website with.

@baobabKoodaa
Copy link
Contributor Author

I wonder if there's a way to ship the non-critical JS assets after shipping the critical images?

@polarathene
Copy link
Contributor

@baobabKoodaa Here's the HTTP/2 dependency graph and Request Details for your site via webpagetest.

You can see priority wise your fonts are quite high, as is the JS, and these priorities are defined by the browser afaik. Using priority hints as @itmayziii helpfully linked to should be a way to improve prioritizing the loading of your image resource, critical should probably add that attribute in a similar way in gatsby-image. I have raised an issue for such here

I know you said it's a HTTP2/resource prioritization issue, but a simple website just shouldn't be bundled with so much JavaScript that loading critical assets is delayed by 500ms. One of the main selling points of Gatsby is that it's supposed to improve your web site performance for you. Here it's doing literally the opposite of that.

Your comparison isn't apples to apples though. Gatsby provides static upfront but then also provides JS to hydrate a React app for additional benefits to the web experience.

The critical prop should include the image markup so that JS isn't required to add it when JS is available, thus you can possibly remove the JS if you have no interest in hydrating to a React app and keeping it purely static(another option might be to use a service like prerender.io? CloudFlare also offers a similar cached copy, though I'm not sure if that'd ignore the js loading/prioritization(I've not used either of these services).

Something you could try is editing the preload tags for the JS in the html head to be prefetch instead. That should change the priority to fetching them after anything more important is loaded in. Or as mentioned you can use HTTP/2 Server Push at the expense of losing browser cache hits(for that particular pushed asset).

If switching to prefetching works for your needs, I'd suggest raising a new issue about it, so that the core devs can consider supporting it via gatsby-config.js to handle it for you as opt-in. It could potentially even query the Web API for saveData which some browsers(particularly mobile devices) can enable so that the JS could have lower priority this way on slow connections perhaps?

Based on the Request Details link, using domain sharding in your case may get your images to arrive sooner.


Other than all that above, I don't see how Gatsby can help you with this. The priority hints are fairly new and afaik only supported in Chrome? You can lower priority of the JS preloads like that link shows instead of increasing priority for specific images if you like.

Ultimately though, this is the purpose of having placeholders in the first place.

@baobabKoodaa
Copy link
Contributor Author

Hey, thanks again. I really appreciate the help. I tried to find the preload tags you were talking about, but it looks like after Gatsby compiles my site, there are just regular script tags without preload in index.html. Do you mean adding a prefetch attribute to those?

@polarathene
Copy link
Contributor

@baobabKoodaa that's odd, I have plenty of preload tags in the head of my index.html, but only if I do gatsby build, output of gatsby develop is different.

Which were you using?

@baobabKoodaa
Copy link
Contributor Author

Oops! Ok so here is 2 versions of my site, one direct output of gatsby build and the other modified so that every script preload in index.html is changed into prefetch:

https://test-preload.netlify.com
https://test-prefetch.netlify.com

At least on latest Chrome on Ubuntu the behavior is same for both: the main image loading is delayed by >500ms because of JS. I don't understand why this happens. Isn't the browser supposed to fetch the prefetch resources only after everything has been fetched for the current navigation? Does it somehow figure out that even though we marked these resources as prefetch, we actually need them on this current page, so it prioritizes them highly despite the prefetch attribute?

@polarathene
Copy link
Contributor

Isn't the browser supposed to fetch the prefetch resources only after everything has been fetched for the current navigation?

Typically yes. Did you inspect the markup?

I see the images are only defined via noscript tags. That suggests no image was added with the critical prop? You have to remember that without that, when JS is available the noscript tag is ignored and thus there is no image markup to process until the JS is downloaded and hydrates into a React app to mount the actual images.

@baobabKoodaa
Copy link
Contributor Author

baobabKoodaa commented May 7, 2019

I see the images are only defined via noscript tags. That suggests no image was added with the critical prop? You have to remember that without that, when JS is available the noscript tag is ignored and thus there is no image markup to process until the JS is downloaded and hydrates into a React app to mount the actual images.

I am adding the top image as critical:

<Picture fluid={fluid} critical={index==0}/> (Source)

I confirmed with a console log that it works as intended (yields true for the top image, false for other images).

I don't know why gatsby-image still puts it inside noscript tags.

I guess what I should try next is maybe replace gatsby-image with my own class (or even just <img>) and see if that improves image loading time.

@polarathene
Copy link
Contributor

I don't know why your critical prop isn't working as expected, I haven't tried compiling the linked source.

I checked with one of my own local projects and adding the critical prop works as expected for producing the correct markup out of noscript tag like mentioned.

I don't know why gatsby-image still puts it inside noscript tags.

Those will be there regardless afaik, although they probably shouldn't be I guess if critical is used.

I guess what I should try next is maybe replace gatsby-image with my own class (or even just ) and see if that improves image loading time.

Yeah, I guess just trying a regular img tag in your markup(or copying it out of the noscript tag) should help test to see if it downloads without the 500ms delay with the JS resources prefetched?

@KyleAMathews
Copy link
Contributor

I think the right solution here is to use add <link preload> for critical images. I definitely agree that critical images should be loaded before JavaScript. I'll create a new issue for this.

@polarathene
Copy link
Contributor

That alone wouldn't raise it's priority over the JS assets being preloaded for hydration into a react app though? JS has priority over images last I knew?

@KyleAMathews
Copy link
Contributor

It's complicated :-D https://blog.cloudflare.com/better-http-2-prioritization-for-a-faster-web/

Adding a preload would at least make the critical image equal priority with JS.

@polarathene
Copy link
Contributor

Yeah I just came across that new article, will read through it and a few others I've got up on the topic before putting together my input on the related issue you opened up.

I am not sure where I saw the information prior, but there was a nice table(at least for Chrome) that showed the prioritization rules for resources by type. Which iirc, while different asset types had their own priority values/levels, this order was still used when deciding order for assets given equal priority?

Priority hints should assist in future if they become more widely adopted. Otherwise domain sharding may sometimes work, or HTTP/2 server push outside of Gatsby for enforcing priority.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: question or discussion Issue discussing or asking a question about Gatsby
Projects
None yet
Development

No branches or pull requests

6 participants