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

[WOM-5377][BpkComponentCalendar] Optimise date formatting for improved INP #3695

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

gwright170
Copy link

@gwright170 gwright170 commented Dec 13, 2024

Address performance issues when instantiating BpkComponentCalendar related to excessive date formatting (many times for 13 months worth of individual days). For Skyscanner staff please refer to https://skyscanner.atlassian.net/wiki/x/9eCWSw for complete analysis of the changes:

  • Front-load date formatting in component constructor and make available via state
  • Use formatted date values from state as opposed to formatting on demand
  • Do not require date formatting to compare dates (reducing the number of times we format dates)
  • Make iso formatted dates available to consumers to reduce their need to format dates
  • Defer generation of date aria-labels until after initial render
  • memoize formatDateFull in composeCalendar

Note

Deferring generation of date aria-labels is a workaround for a poorly performing date format function from saddlebag-localisation that Skyscanner are mandated to use - see test-localisation-perf. This solution isn't inherently bad (it's a method used by React Fibre for prioritising render) but adds complexity and indicates wider performance issues we should aim to resolve.

Remember to include the following changes:

  • Ensure the PR title includes the name of the component you are changing so it's clear in the release notes for consumers of the changes in the version e.g [KOA-123][BpkButton] Updating the colour
  • README.md (If you have created a new component)
  • Component README.md
  • Tests
  • Accessibility tests
    • The following checks were performed:
      • Ability to navigate using a keyboard only
      • Zoom functionality (Deque University explanation):
        • The page SHOULD be functional AND readable when only the text is magnified to 200% of its initial size
        • Pages must reflow as zoom increases up to 400% so that content continues to be presented in only one column i.e. Content MUST NOT require scrolling in two directions (both vertically and horizontally)
      • Ability to navigate using a screen reader only
  • Storybook examples created/updated
  • For breaking changes or deprecating components/properties, migration guides added to the description of the PR. If the guide has large changes, consider creating a new Markdown page inside the component's docs folder and link it here

- reduce usage of date formats
Copy link

Visit https://backpack.github.io/storybook-prs/3695 to see this build running in a browser.

Copy link

Visit https://backpack.github.io/storybook-prs/3695 to see this build running in a browser.

@gwright170 gwright170 changed the title WOM-000 | Front-load date formatting for better INP WOM-000 | Optimise date formatting for optimal Interaction to Next Paint (INP) Dec 16, 2024
@gwright170 gwright170 changed the title WOM-000 | Optimise date formatting for optimal Interaction to Next Paint (INP) WOM-000 | Optimise date formatting for optimal Interaction to Next Paint Dec 16, 2024
@gwright170 gwright170 changed the title WOM-000 | Optimise date formatting for optimal Interaction to Next Paint WOM-000 | Optimise date formatting for optimal INP Dec 16, 2024
@gwright170 gwright170 changed the title WOM-000 | Optimise date formatting for optimal INP [WOM-5377] Optimise date formatting for optimal INP Dec 16, 2024
@gwright170 gwright170 changed the title [WOM-5377] Optimise date formatting for optimal INP [WOM-5377][BpkComponentCalendar] Optimise date formatting for improved INP Dec 18, 2024
Copy link

Visit https://backpack.github.io/storybook-prs/3695 to see this build running in a browser.

Copy link

github-actions bot commented Dec 18, 2024

Warnings
⚠️

Package source files (e.g. packages/package-name/src/Component.js) were updated, but snapshots weren't. Have you checked that the tests still pass?

⚠️

Package source files (e.g. packages/package-name/src/Component.tsx) were updated, but type files weren't. Have you checked that no types have changed?

Browser support

If this is a visual change, make sure you've tested it in multiple browsers.

Generated by 🚫 dangerJS against ea2221f

Copy link

Visit https://backpack.github.io/storybook-prs/3695 to see this build running in a browser.

Copy link

Visit https://backpack.github.io/storybook-prs/3695 to see this build running in a browser.

1 similar comment
Copy link

Visit https://backpack.github.io/storybook-prs/3695 to see this build running in a browser.

@gwright170 gwright170 added the minor Non breaking change label Dec 18, 2024
@gwright170 gwright170 marked this pull request as ready for review December 18, 2024 13:54
@@ -185,6 +186,7 @@ class BpkCalendarDate extends PureComponent<Props> {
}

delete buttonProps.preventKeyboardFocus;
delete buttonProps.formattedDate;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensures prop isn't passed to html button, invalidating existing snapshots

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain a little more what is going on here, I'm not sure I understand.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The returned component includes {...buttonProps}, so the html includes formattedDate="2024-12-19". This isn't an issue but:

  • wasn't intentional
  • invalidates all the existing snapshot tests

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, button props is derived from spreading the rest of this.props. Where does this component end up getting used? I can't find where we end up needing the new formattedDate prop, but I've not got a good picture of the Component hierarchy here.

Copy link
Author

@gwright170 gwright170 Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my case it is used in the Flights Search Controls GC to prevent re-generating iso date labels - https://github.skyscannertools.net/skyscanner/global-components/pull/1607/files

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't understand, the prop is passed into the component, it's not referenced anywhere nor is this.props passed into anything then its deleted from buttonProps before it's used. How does including this prop in this component do anything?

Copy link
Author

@gwright170 gwright170 Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies. The Global Component uses a custom Date component:
composeCalendar(null, BpkCalendarGridHeader, BpkScrollableCalendarGridList, Date).

We pass formattedDate to the Date component. In the case of the GC this value is valuable, we use it to prevent duplicate generation of iso formatted dates (these dates are used to colour code dates with prices).

In the case of the default Backpack Date component that formattedDate value is not useful, we delete it from the props to avoid it appearing in the HTML and snapshots.

Comment on lines 124 to 134
componentDidMount(): void {
deferCallback(() =>
this.setState({
calendarMonthWeeks: getCalendarMonthWeeks(
this.props.month,
this.props.weekStartsOn,
this.props.formatDateFull,
),
}),
);
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the constructor we get dates and formatted labels for everything but the aria-label. Here we regenerate dates, this time with aria labels, and defer until the thread is idle (typically after render). This change reduces INP by roughly 20%.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes in this file cover simplification of date comparison. There were a few places we needlessly formatted dates for comparison. This change reduces INP by roughly 20%.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consistent memoization of formatDateFull (no need for consumers to implement this now)

Comment on lines 120 to 122
val: currDate,
textLabel: formatDate ? formatDate(currDate) : 'loading...',
isoLabel: formatIsoDate(currDate),
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perform all date formatting in a single place, rather than on demand across many files. This decreases the number of date format calls requires and contributes to a 20% reduction in INP.

Comment on lines +25 to +26
const element = document.createElement('div');
element.scrollIntoView = jest.fn();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was broken but was not being run because the test pattern specifies -test rather than .test. Renamed and fixed.

*/

/**
* Mock deferCallback to immediately invoke the callback. This ensures that
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See JSdoc

*/

/**
* Defer a task until the browser is idle, or till the next "tick" if the
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See JSdoc

registerRequireContextHook();

jest.mock('../../packages/bpk-react-utils/src/deferCallback');
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Globally mock deferCallback to ensure calendar snapshots include real aria label on initial render, not 'loading...' fallback.

scripts/jest/setup.js Show resolved Hide resolved
packages/bpk-react-utils/index.ts Outdated Show resolved Hide resolved
packages/bpk-react-utils/src/deferCallback.ts Outdated Show resolved Hide resolved
packages/bpk-react-utils/src/deferCallback.ts Show resolved Hide resolved
packages/bpk-component-calendar/src/BpkCalendarGrid.tsx Outdated Show resolved Hide resolved
@@ -185,6 +186,7 @@ class BpkCalendarDate extends PureComponent<Props> {
}

delete buttonProps.preventKeyboardFocus;
delete buttonProps.formattedDate;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain a little more what is going on here, I'm not sure I understand.

packages/bpk-component-calendar/src/date-utils.ts Outdated Show resolved Hide resolved
Comment on lines +101 to +111
function getCalendarNoCustomLabel(date: Date, weekStartsOn: number) {
return getCalendarMonthWeeks(date, weekStartsOn);
}

function getCalendar(
date: Date,
weekStartsOn: number,
formatDate: (d: Date) => Date | string,
) {
return getCalendarMonthWeeks(date, weekStartsOn, formatDate);
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appreciate this could be cleaner but:

  • I wanted to avoid separating the values into new state to avoid more refactoring and complexity to rationalise back together
  • Mapping the values in after the initial generation is arguably slower and harder to read

Copy link

Visit https://backpack.github.io/storybook-prs/3695 to see this build running in a browser.

Copy link

Visit https://backpack.github.io/storybook-prs/3695 to see this build running in a browser.

Copy link

Visit https://backpack.github.io/storybook-prs/3695 to see this build running in a browser.

@gwright170 gwright170 added major Breaking change and removed minor Non breaking change labels Dec 19, 2024
@gwright170 gwright170 added minor Non breaking change and removed major Breaking change labels Dec 19, 2024
Copy link

Visit https://backpack.github.io/storybook-prs/3695 to see this build running in a browser.

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

Successfully merging this pull request may close these issues.

3 participants