Conversation
83015b5 to
27ed077
Compare
|
@legrego I added lazy-loading for our reusable UI components in 27ed077 but it did not positively impact the page load bundle for the SOM and ML plugins. Perhaps we only get the performance benefit if we statically import components? Snapshot of the Kibana Machine comment:
|
|
So, here are the metrics for Phase 2.5 after we added the reusable UI components:
The ML and SOM bundles never increased, but the Spaces bundle did. So I guess what you're saying here is that our goal is to decrease the Spaces page load bundle? That makes sense. I'll see if I can tinker with it a bit more. |
Yes my goal was to decrease the Spaces page load bundle. I wasn't concerned about the ML/SOM bundles here |
This is needed to use the import sorter
Sort order 1. External dependencies 2. Internal absolute dependencies (@kbn/..., src/...) 3. Internal relative dependencies Also cleaned up some relative dependencies in the process.
Adds ESLint rule to enforce this.
Splits out most components into separate chunks
An error was getting logged to the console because jsdom does not support window.location.reload() out of the box.
Rename it from 'CopySavedObjectsToSpaceFlyout' to 'CopyToSpaceFlyoutInternal'.
Rename it from 'SpaceAvatar' to 'SpaceAvatarInternal'
This was statically imported by the Security plugin so it required quite a bit of refactoring.
Reduces page load bundle by 29KB
Thought this would decrease the page load bundle size, but it only shaved off a few hundred bytes. At any rate, this change cleans up the code a bit, and will be needed when we eventually expose this as a reusable component for outside consumers.
27ed077 to
b8d93b0
Compare
|
|
||
| const { SpaceList, ShareToSpaceFlyout } = spacesApi.ui.components; | ||
| const LazySpaceList = React.lazy(spacesApi.ui.components.getSpaceList); | ||
| const LazyShareToSpaceFlyout = React.lazy(spacesApi.ui.components.getShareToSpaceFlyout); |
There was a problem hiding this comment.
Dynamic imports using React.lazy should be created outside the render function, otherwise you're creating a new component with every render of JobSpacesList
There was a problem hiding this comment.
There isn't really anything 'outside' of render in a FC 😅 . But useMemo should be used around the React.lazy declaration, yea (as it was properly done in saved_objects_table_page.tsx)
Highly depends on the answers on #91976 (comment) though.
There was a problem hiding this comment.
I just pushed some changes.
Wherever I could do so (inside the Spaces plugin) I pulled out the lazy loading to the top level.
Otherwise, I memoized these inside of function components.
|
|
||
| return ( | ||
| <> | ||
| <React.Suspense fallback={<EuiLoadingSpinner />}> |
There was a problem hiding this comment.
Suspense will throw an error if the lazy component cannot be loaded so you might want to wrap this in an error boundary and create a fallback.
There was a problem hiding this comment.
This component is only rendered if the Spaces plugin is enabled and the Spaces API is available, and the getters to lazy load these components (getSpaceList, getShareToSpaceFlyout) are just promises to import the components that we are already guaranteed to have available. IMO we don't need to complicate this with an error boundary and fallback when we can rely on the Kibana platform's public contracts.
There was a problem hiding this comment.
As discussed offline -- this is necessary if the user runs into any network errors at load time 😅 I'll add wrappers for these that display a toast message so the whole page doesn't blow up.
Edit: done in 032b5cb.
| spacesApi ? spacesApi.ui.components.getSpacesContext : getEmptyFunctionComponent | ||
| ), | ||
| [spacesApi] | ||
| ); |
There was a problem hiding this comment.
I probably don't have enough context (lol, see what I did there?) so might be better to catch up tomorrow but it would be nice to be a bit clearer here in regards to naming.
getSpacesContext to me sounds like we're getting a React Context object for spaces. That's usually a static object which contains a Provider and a Consumer component. In this case however, we are wrapping it in React.lazy to create a lazy loaded component so it's just the Provider component by the looks of it? In that case it might be clearer to call the method getSpacesProvider and the return value SpacesProvider.
Having said that, Context (returned by createContext) is usually a static object in React so we could just make that available through the spaces plugin contract directly rather than having these getter functions and having to memoize them.
Anyways, let's catch up tomorrow, just adding this as a reminder.
There was a problem hiding this comment.
Yea, I share the same concerns. React context providers are usually static, and the value they provide mutates. I'm not sure to understand why we need an lazy accessor around a context provider?
There was a problem hiding this comment.
it would be nice to be a bit clearer here in regards to naming.
I'm open to naming changes!
getSpacesContextto me sounds like we're getting a React Context object for spaces. That's usually a static object which contains a Provider and a Consumer component. In this case however, we are wrapping it inReact.lazyto create a lazy loaded component so it's just the Provider component by the looks of it?
Sort of. It's a "wrapper" component that includes the context provider. We need this because when this wrapper is mounted, it makes a series of API calls and caches the results, then makes those available to its children via the context provider.
The function to obtain this wrapper within the Spaces plugin is called getSpacesContextWrapper, but the one that is exposed to consumers in the public contract is called getSpacesContext. We could rename the latter to something like getSpacesContextProviderWrapper but I don't know what value that really adds for consumers, it's an implementation detail IMO. They don't need to consume the context provider, they just need to know that they have to render the flyout, space list, etc. as a descendant of this wrapper.
Having said that, Context (returned by
createContext) is usually a static object in React so we could just make that available through the spaces plugin contract directly rather than having these getter functions and having to memoize them.
I don't think so, we need the wrapper.
I'm not sure to understand why we need an lazy accessor around a context provider?
Because it's not just a context provider, it's the wrapper that also fetches data. Also, to flip that question around: why not provide a lazy accessor? All of the other components have lazy accessors. It seems this approach is more consistent.
There was a problem hiding this comment.
Caught up with Joe and I can't think of a good alternative to avoid the architectural complexity with the approach here.
To fulfil the brief, we need a way to make the spaces state available globally, so that we can provide drop-in components for developers that share that state. However, we're not able to render the context provider at the root level of the application ourselves (since spaces is a plugin) and we can't provide static exports (due to the restriction imposed by the platform), hence the indirection using dynamically created and memoized context providers, that require developers to consume them using the spaces plugin contract rather than static imports like EUI.
I'm guessing this is a bit of an edge case and other plugin developers don't have the same requirements to share core functionality like spaces does. However, I would argue that certain things like i18n, routing, session information, authenticated user information and in this particular case spaces are global concerns that should be available throughout the application. Usually these are managed at the root level in React applications so that any child component has access via hooks and context but this isn't really possible with the way we've currently modelled spaces as a plugin.
Happy to leave this as is for now but I am slightly worried about the complexity and maintainability of the current solution.
Components exposed in the public contract are now wrapped and automatically lazy-loaded. Consumers no longer have to handle this.
If we have an exception where we do not use an error boundary, I added a comment explaining why. I also standardized our usages of `lazy` and `Suspense` by destructuring the React import.
thomheymann
left a comment
There was a problem hiding this comment.
Looks good, couple of comments related to your usage of hooks.
...ublic/management/roles/edit_role/privileges/kibana/privilege_summary/space_column_header.tsx
Show resolved
Hide resolved
x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx
Outdated
Show resolved
Hide resolved
...k/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_action.tsx
Show resolved
Hide resolved
...k/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_column.tsx
Show resolved
Hide resolved
| getStartServices().then(([coreStart]) => { | ||
| setNotifications(coreStart.notifications); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Again, the side effect here will run at every render unless you add a dependency array and you also need to ensure the component is still mounted when modifying state asynchronously.
What you could do so you don't have to manage all that yourself is use the useAsync hook from react-use module:
const { value: { notifications } } = useAsync(getStartServices); // `value` is the `coreStart` object returned by `getStartServices`
return <SuspenseErrorBoundary notifications={notifications} />https://streamich.github.io/react-use/?path=/story/side-effects-useasync--docs
There was a problem hiding this comment.
Good idea, thanks!
Destructuring makes this a bit funky, but this is what I did to make it work:
const { value: startServices = [{ notifications: undefined }] } = useAsync(getStartServices);
const [{ notifications }] = startServices;
x-pack/plugins/spaces/public/suspense_error_boundary/suspense_error_boundary.tsx
Show resolved
Hide resolved
|
@elasticmachine merge upstream |
| setNotifications(coreStart.notifications); | ||
| }); | ||
| }); | ||
| const { value: startServices = [{ notifications: undefined }] } = useAsync(getStartServices); |
There was a problem hiding this comment.
For next time: An empty object should be enough here:
const { value: startServices = [{}] } = useAsync(getStartServices);
There was a problem hiding this comment.
Ah, I did this because there's no type overlap between CoreStart and {}, so tsc complains about that. Using { notifications: undefined } allows for type inferencing. Now that you mention it though I suppose this would work
const { value: startServices = [{} as CoreStart] } = useAsync(getStartServices);I think it's a bit better to have explicit types but we've got a big mix of inferences + explicit typing everywhere 🙈
💚 Build Succeeded
Metrics [docs]Module Count
Async chunks
Page load bundle
History
To update your PR or re-run it, just comment with: |
💔 Backport failed❌ 7.x: Commit could not be cherrypicked due to conflicts To backport manually, check out the target branch and run: |
… ilm/rollup-v2-action * 'ilm/rollup-v2-action' of github.com:elastic/kibana: [Security Solution][Case][Bug] Only add rule object for alert comments (#92977) [Security Solution][Case] Show the current connector name in case view (#93018) [Security Solution] Remove unused mock data (#92357) Adds mapping to the signals for the indicator rules that were missing (#92928) skip flaky suite (#85208) Cleanup spaces plugin (#91976) Control round and decimal places in Gauge Visualization when using aggregate functions like average (#91293) Added alerting ui mock for jest test (#92604) Remove "beta" label from URL Drilldown as it is now GA (#92859)

Paying down technical debt on the Spaces plugin:
import typedirectiveSpaceAvatarcomponent