WICG / soft-navigations

Heuristics to detect Single Page Apps soft navigations
https://wicg.github.io/soft-navigations/
Other
46 stars 6 forks source link

Whats the startTime of a softnav? #14

Closed paulirish closed 10 months ago

paulirish commented 1 year ago

Looking at the impl in Blink it looks like it's a DOMHighResTimeStamp that's just a tad before Event Timing's processingStart.. essentially right before the event handlers execute.

But I'm wondering if it should be more like Event Timing's startTime which is the "hardware" timestamp of the event.. ?

The two relevant bits from event timing:

The timestamp at which the event was created can be obtained via the event’s timeStamp. In addition, performance.now() could be called both at the beginning and at the end of the event handler logic. By subtracting the hardware timestamp from the timestamp obtained at the beginning of the event handler, the developer can compute the input delay: the time it takes for an input to start being processed. [...] An Event's delay is the difference between the time when the browser is about to run event handlers for the event and the Event's timeStamp. The former point in time is exposed as the PerformanceEventTiming's processingStart, whereas the latter is exposed as PerformanceEventTiming's startTime. Therefore, an Event's delay can be computed as processingStart - startTime.

A trace showing where the timestamp currently is. The timestamp used for the softnav perfEntry === the SoftNavigationHeuristics::UserInitiatedClick event.

image
paulirish commented 1 year ago

Ah, I found 1369680 - SoftNavigationEntry timestamp should be the click event start - chromium which has this discussion.

I'm reading mocny's argument as: hard navs don't start with the hardware timestamp of the event that triggered... so.. we shouldn't do the same for softnavs.

It seems reasonable, but I don't think I agree. IMO consistency alone isn't a good enough reason. (At the very least, I agree with yoav's "ideally, we'd expose the hardware timestamp numbers for both soft navigations and MPA followup navigations")

Using the user-centric framing…

image

FCP is the "Is it happening?" moment where the user gets feedback that the navigation has successfully begun. The payoff is the paint, and the start of that is the start of the interaction.

The case of massive input delay just means that the user's click has a big interaction latency until paint.. but IMO the fact that the cost is in input delay and not eventProcessing doesn't seem relevant to them. So the most user-centric POV of a softnav should start with the hardware timestamp.

mmocny commented 1 year ago

Valid points.

What about more complex cases, like this situation:

For conext, CLS would (currently, at least) use the processing start time of the event in order to set its 500ms hadRecentInput window.

I agree it would be good if all of CLS, INP, FCP/LCP started at the same time point. I do agree that for a User, the "latency" starts with timeStamp -- especially so for SPA or Same-origin MPA navigations... but it feels odd to blame the next origin document FCP for the previous origin Input Delay... (I know, we sorta do already for unload, redirects...).

Maybe we don't need to be consistent, but there's lots we'd need to change to be ideal...

mmocny commented 1 year ago

Oh, and, we have some efforts ongoing to make INP measure only up to the start of the new navigation-- so in my mind we would have:

INP (outgoing interaction) --> FCP (incoming navigation).

So any "navigation" needs to be 200+1500 ms at most, with yielding/rendering feedback at least once in between. That seems consistent with MPA, at least.

With that perspective, Soft-nav FCP should actually start even later, after INP stops measurement...

yoavweiss commented 1 year ago

Apologies for not chiming-in earlier..

I think that we have a couple of conflicting arguments here:

Looking at what we're doing for hard navigations, it seems like we're starting their time origin around creating a new browsing context.

(as an aside, it's not immediately obvious to me how that timestamp related to creating navigation params by fetching which seems to be what implementations are doing. ^^ @noamr)

In any case, I believe the relevant timestamp for hard navigations is after the initial event handler (e.g. for an <a> element's "click" event on the previous page) fired, and the browser was notified by the renderer that it's navigating. As such, the event start time seems to have better equivalence.

Moving both of those timestamps to the hardware timestamp would be ideal, but would also present many practical challenges:

yoavweiss commented 10 months ago

@paulirish - thoughts on the above? Do you think we can close this?

mmocny commented 10 months ago

Throwing in one more option: beforeunload event timing could be a useful model to look at (see: https://github.com/w3c/navigation-timing/issues/190).

It is already well specced relative to interactions that would start navigations. For Same Origin document navigations -- which is what we are comparing for soft-navs -- my understanding is that the end of beforeunload event is (more or less?) the timeOrigin for the incoming page.

I know soft-navs won't actually fire that event, so I just mean to use as a model. I think the navigation API, when it intercepts the navigation, is basically the equivalent?


Another advantage of using that timestamp (i.e. one that follows the end of interaction processing) is that it makes it easier to slice the page timeline based on timestamps alone. Any entry (layout shift, event timing, etc) which preceded the start of the soft-nav you can be sure were from the previous route. If you use the Event timeStamp, even though it might capture User Experience, there becomes an overlapping amount of time where you are still on the previous route (i.e. the input delay portion, at the very least, but event processing + navigation event scheduling as well).

Given that INP already measures the latency of the "last" interaction with the previous route, I think its fine to say that navigation latency is INP+LCP and that you have 200ms to respond and 2.5s to load.

noamr commented 10 months ago

Navigation timing doesn't use the time after beforeunload anymore, but rather the startTime of the fetch, as it's almost the same timestamp and makes it equivalent to resource timing.

ATM I can't think of a good conceptual equivalent in SPA - in both of them there could be some client-side processing in an event handler before we even know it's a navigation (think of a click handler that runs some JS before setting location.href.

We can't use anything like the beforeunload scenario etc because this is not a navigation in the HTML standard sense (assuming we're not using the navigation API) - all we're doing is responding to a UI event and changing the DOM+URL, and we're deriving from that that this is a "navigation".

Side note, there's a misconception that somehow the start time of navigation timing correlates directly with UX, but because of the above this is only true in some of the cases.

yoavweiss commented 10 months ago

In my view, the timestamp of the event that handles the user interaction (which is the currently defined behavior) is a reasonable compromise between the two extremes of hardware timestamp on the one hand, and start of the relevant fetch() on the other.

If y'all disagree, I'm happy to take concrete suggestions :)

noamr commented 10 months ago

In my view, the timestamp of the event that handles the user interaction (which is the currently defined behavior) is a reasonable compromise between the two extremes of hardware timestamp on the one hand, and start of the relevant fetch() on the other.

Sounds reasonable, but it comes down to whether you want to compare between soft and hard navigations. I think with this model you can't in many scenarios.

yoavweiss commented 10 months ago

I think with this model you can't in many scenarios.

Is there a model with which it is possible?

noamr commented 10 months ago

I think with this model you can't in many scenarios.

Is there a model with which it is possible?

I guess not. This is OK then.

mmocny commented 10 months ago

I continue to think that navigation UX should be considered as a combo of outgoing INP (after all, the outgoing page should still provide feedback for the interaction) + incoming FCP/LCP.

With that perspective, starting timeOrigin at the processingStart time of the event handler feels too early, given that it overlaps with what is already measured by INP. Yes, some setup may be kicked off there but substantial work should be deferred until after-next-paint and good SPAs do that by default. (i.e. defer incoming route rendering at least one animation frame, potentially with a transition).

INP likely needs to change slightly, and should stop at first visual feedback, which for navigations may not necessarily be Next Paint. It may suffice to consider URL change / UA loading spinners as sufficient to mark the transition point, which is why I think that navigate event is the right time point (the equivalent of beforeUnload).

For MPA navigations INP is moving in that direction as well: it should end measurement if page lifecycle events ("unload") fire before next paint has a chance.

mmocny commented 10 months ago

Hmmm, I realize now that only works for cases of eager URL updates, which is the model the navigate event supports when used to intercept 'semantic' navigations like <a> clicks, etc.

But in the case where a navigation is a custom click handler + preventDefault, the url change could be applied lazily. In those cases navigate is too late. Then I guess you do need to fallback to the original input event timing (via task attribution).

Is this correct?: Screenshot 2023-11-21 at 10 32 20

If so, perhaps:

But I can see the appeal of just using processingStart to keep it simple.

mmocny commented 10 months ago

After some discussion and testing, there seems to be agreement in the direction of adjusting the soft-nav startTime to be either:

In some local testing, this seems to match perceived UX well, seems useful for attribution (i.e. the INP vs FCP/LCP split), and has a nice measurement symmetry with MPA navigations.

(we may want to double check exactly which time point to use for navigate event. Perhaps the intercept time could differ from processingEnd time?)

mmocny commented 10 months ago

I have been doing some local testing with the following:

Add this to the outgoing page:

function cleanTime(ms = performance.timeOrigin + performance.now()) {
  return ms.toLocaleString(undefined, { maximumFractionDigits: 2 });
}

function block(ms) {
    const target = performance.now() + ms;
    while (performance.now() < target);
}

// Note: console doesn't work from page lifecycle events.
function longEvent(event) {
  console.log(event.type, "start", cleanTime());
  block(1000);
  localStorage['Soft.'+event.type] = cleanTime();
  console.log(event.type, "end", cleanTime());
}

navigation.addEventListener('navigate', longEvent);
window.addEventListener('pagehide', longEvent);
document.addEventListener('visibilitychange', longEvent);

console.log('timeOrigin', cleanTime(performance.timeOrigin));

Then I navigate around cross origin and same origin, injecting that snipped and comparing the next page timeOrigin to the previous page navigateEnd.

I try variants of navigations:

...and it seems fairly consistent that navigate end is a reasonable and consistent timeOrigin for navigations.

yoavweiss commented 10 months ago

While implementing this, a question (read: failing test) has come up: When there's a click event handler that then called history.back() and triggers a popstate event, which of these events should set the timestamp as its processing end?

I'd argue that in that case, it's the click event that should be set. But I'd love your thoughts if you think that's wrong.

mmocny commented 10 months ago

Net/net, I think it still fits the model of "event processingEnd" or "navigate event end", whichever comes first.


I just tested locally, and I noticed that the behaviour of back() appears to differ for same-origin and cross-origin navigations (cross-document), and seems also to differ even further for SPA (at least a few I tested).

I think this is somewhat equivalent to using window.location for forward navigations?

Unlike pushState(), which will dispatch synchronously from the middle of the event handlers, and update URL and fire navigate event, which back() and location = href will schedule those events to fire after event processing, and delay URL update.

I think that means its consistent.


[1]: I also thought that there were some conventions/interventions about being able to intercept/observe specific to back navigations: e.g. maybe you can intercept once per new user interaction with a page, and so maybe that explains some of the nav event firing order? However, I'm not seeing any difference if I interact with the web content first or not. I may not be testing perfectly.

mmocny commented 10 months ago

Oh, maybe your point was that the navigation doesn't "actually start" until popstate event is scheduled, and so we could choose to treat that event as "the event" instead of the click?

I think I agree that click event is still the right one.

Although I suspect most pages won't actually start rendering until popstate fires, and URL doesn't update until it does, I would consider this a type of "TTFB" for the navigation. This delay won't be captured by INP (or isn't guaranteed to, anyway)

yoavweiss commented 10 months ago

Oh, maybe your point was that the navigation doesn't "actually start" until popstate event is scheduled, and so we could choose to treat that event as "the event" instead of the click?

I think I agree that click event is still the right one.

That was indeed my question! I'm glad we agree :)

yoavweiss commented 10 months ago

This is now closed by https://github.com/WICG/soft-navigations/commit/1111c036db3051bc16c60542e42e6af24dddf571, so closing! Thanks for the discussion!!

yoavweiss commented 10 months ago

Also, https://chromium-review.googlesource.com/c/chromium/src/+/5059108 is the Chromium implementation change to align with this.