From 6512e18ea39511afdbb6a001205f4ea07863f69f Mon Sep 17 00:00:00 2001 From: "Jules Sam. Randolph" Date: Thu, 18 Feb 2021 14:28:56 -0300 Subject: [PATCH] perf: limit rerenderings via memoization --- packages/render-html/src/RenderHTML.tsx | 2 + .../render-html/src/RenderHTMLFragment.tsx | 53 ++++------- .../render-html/src/RenderResolvedHTML.tsx | 15 +++ .../render-html/src/TDocumentRenderer.tsx | 3 +- .../render-html/src/TNodeChildrenRenderer.tsx | 2 +- packages/render-html/src/TNodeRenderer.tsx | 2 +- .../render-html/src/TRenderEngineProvider.tsx | 2 +- .../src/context/SharedPropsContext.ts | 94 ------------------- .../src/context/SharedPropsProvider.tsx | 83 ++++++++++++++++ .../src/context/defaultSharedProps.ts | 34 +++++++ .../src/helpers/selectSharedProps.ts | 6 +- .../src/hooks/useAssembledCommonProps.ts | 2 +- packages/render-html/src/index.ts | 2 +- .../render-html/src/renderers/IMGRenderer.tsx | 2 +- packages/render-html/src/shared-types.ts | 2 +- 15 files changed, 162 insertions(+), 142 deletions(-) create mode 100644 packages/render-html/src/RenderResolvedHTML.tsx delete mode 100644 packages/render-html/src/context/SharedPropsContext.ts create mode 100644 packages/render-html/src/context/SharedPropsProvider.tsx create mode 100644 packages/render-html/src/context/defaultSharedProps.ts diff --git a/packages/render-html/src/RenderHTML.tsx b/packages/render-html/src/RenderHTML.tsx index 41c95818b..71822ff12 100644 --- a/packages/render-html/src/RenderHTML.tsx +++ b/packages/render-html/src/RenderHTML.tsx @@ -15,6 +15,8 @@ import RenderHTMLFragment from './RenderHTMLFragment'; * performance. * * @param props - Props for this component. + * + * @public */ export default function RenderHTML(props: RenderHTMLProps) { return ( diff --git a/packages/render-html/src/RenderHTMLFragment.tsx b/packages/render-html/src/RenderHTMLFragment.tsx index cafd8b95a..4fda4d14c 100644 --- a/packages/render-html/src/RenderHTMLFragment.tsx +++ b/packages/render-html/src/RenderHTMLFragment.tsx @@ -1,22 +1,14 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; - -import { - RenderResolvedHTMLProps, - ResolvedResourceProps, - RenderHTMLFragmentProps -} from './shared-types'; -import useTTree from './hooks/useTTree'; -import SharedPropsContext, { - defaultSharedPropsContext -} from './context/SharedPropsContext'; +import { ResolvedResourceProps, RenderHTMLFragmentProps } from './shared-types'; import TChildrenRenderersContext from './context/TChildrenRendererContext'; import TNodeChildrenRenderer from './TNodeChildrenRenderer'; import RenderHTMLFragmentDebug from './RenderHTMLFragmentDebug'; import SourceLoader from './SourceLoader'; import TChildrenRenderer from './TChildrenRenderer'; -import TDocumentRenderer from './TDocumentRenderer'; -import selectSharedProps from './helpers/selectSharedProps'; +import RenderResolvedHTML from './RenderResolvedHTML'; +import SharedPropsProvider from './context/SharedPropsProvider'; +import defaultSharedProps from './context/defaultSharedProps'; export type RenderHTMLFragmentPropTypes = Record< keyof RenderHTMLFragmentProps, @@ -62,26 +54,22 @@ export const renderHtmlFragmentPropTypes: RenderHTMLFragmentPropTypes = { export const renderHTMLFragmentDefaultProps: { [k in keyof RenderHTMLFragmentProps]?: RenderHTMLFragmentProps[k]; } = { - ...defaultSharedPropsContext, + ...defaultSharedProps, contentWidth: undefined }; -function RenderResolvedHTML(props: RenderResolvedHTMLProps) { - const ttree = useTTree(props); - return ( - - ); -} +const childrenRendererContext = { + TChildrenRenderer, + TNodeChildrenRenderer +}; /** * Render a HTML snippet, given that there is a `TRenderEngineProvider` up in * the render tree. * * @param props - Props for this component. + * + * @public */ export default function RenderHTMLFragment(props: RenderHTMLFragmentProps) { const { @@ -90,8 +78,7 @@ export default function RenderHTMLFragment(props: RenderHTMLFragmentProps) { onTTreeChange, remoteErrorView, remoteLoadingView, - onDocumentMetadataLoaded, - ...remainingProps + onDocumentMetadataLoaded } = props; const sourceLoaderProps = { source, @@ -106,20 +93,14 @@ export default function RenderHTMLFragment(props: RenderHTMLFragmentProps) { /> ) }; + return ( - - ({ - TChildrenRenderer, - TNodeChildrenRenderer - }), - [] - )}> + + {React.createElement(SourceLoader, sourceLoaderProps)} - + ); } diff --git a/packages/render-html/src/RenderResolvedHTML.tsx b/packages/render-html/src/RenderResolvedHTML.tsx new file mode 100644 index 000000000..f76de465a --- /dev/null +++ b/packages/render-html/src/RenderResolvedHTML.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import useTTree from './hooks/useTTree'; +import { RenderResolvedHTMLProps } from './shared-types'; +import TDocumentRenderer from './TDocumentRenderer'; + +export default function RenderResolvedHTML(props: RenderResolvedHTMLProps) { + const ttree = useTTree(props); + return ( + + ); +} diff --git a/packages/render-html/src/TDocumentRenderer.tsx b/packages/render-html/src/TDocumentRenderer.tsx index 5cf0446d1..0652bf08f 100644 --- a/packages/render-html/src/TDocumentRenderer.tsx +++ b/packages/render-html/src/TDocumentRenderer.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo } from 'react'; +import React, { memo, useEffect, useMemo } from 'react'; import { TDocument } from '@native-html/transient-render-engine'; import { DocumentMetadata, RenderHTMLFragmentProps } from './shared-types'; import DocumentMetadataProvider from './context/DocumentMetadataProvider'; @@ -52,5 +52,4 @@ const TDocumentRenderer = ({ ); }; - export default TDocumentRenderer; diff --git a/packages/render-html/src/TNodeChildrenRenderer.tsx b/packages/render-html/src/TNodeChildrenRenderer.tsx index db905cace..62a02f133 100644 --- a/packages/render-html/src/TNodeChildrenRenderer.tsx +++ b/packages/render-html/src/TNodeChildrenRenderer.tsx @@ -5,7 +5,7 @@ import { TPhrasing, TText } from '@native-html/transient-render-engine'; -import { useSharedProps } from './context/SharedPropsContext'; +import { useSharedProps } from './context/SharedPropsProvider'; import TChildrenRenderer, { tchildrenRendererDefaultProps } from './TChildrenRenderer'; diff --git a/packages/render-html/src/TNodeRenderer.tsx b/packages/render-html/src/TNodeRenderer.tsx index c4a584d80..24474b39c 100644 --- a/packages/render-html/src/TNodeRenderer.tsx +++ b/packages/render-html/src/TNodeRenderer.tsx @@ -10,7 +10,7 @@ import TPhrasingRenderer from './TPhrasingRenderer'; import TTextRenderer from './TTextRenderer'; import { Markers, TNodeRendererProps } from './shared-types'; import { getMarkersFromTNode } from './helpers/getMarkersFromTNode'; -import { useSharedProps } from './context/SharedPropsContext'; +import { useSharedProps } from './context/SharedPropsProvider'; export type { TNodeRendererProps } from './shared-types'; diff --git a/packages/render-html/src/TRenderEngineProvider.tsx b/packages/render-html/src/TRenderEngineProvider.tsx index acd5fcb55..9510349e9 100644 --- a/packages/render-html/src/TRenderEngineProvider.tsx +++ b/packages/render-html/src/TRenderEngineProvider.tsx @@ -80,7 +80,7 @@ export function useAmbiantTRenderEngine() { /** * A react component to share a transient web engine instance across different - * rendered contents via `RenderHTMLFragment`. This can seriously enhance + * rendered contents via `RenderHTMLFragment`. This can significantly enhance * performance in applications with potentially dozens or hundreds of distinct * rendered snippets such as chat apps. * diff --git a/packages/render-html/src/context/SharedPropsContext.ts b/packages/render-html/src/context/SharedPropsContext.ts deleted file mode 100644 index 84777b93b..000000000 --- a/packages/render-html/src/context/SharedPropsContext.ts +++ /dev/null @@ -1,94 +0,0 @@ -import React, { useCallback } from 'react'; -import { Dimensions, Linking, TextProps, ViewProps } from 'react-native'; -import { RenderHTMLSharedProps, TRendererBaseProps } from '../shared-types'; - -export const defaultSharedPropsContext: Required = { - debug: false, - contentWidth: Dimensions.get('window').width, - enableExperimentalPercentWidth: false, - defaultTextProps: { - selectable: false, - allowFontScaling: true - }, - defaultViewProps: {}, - enableExperimentalMarginCollapsing: false, - computeEmbeddedMaxWidth: (contentWidth) => contentWidth, - imagesInitialDimensions: { - height: 50, - width: 50 - }, - onLinkPress: (_e, href) => Linking.canOpenURL(href) && Linking.openURL(href), - WebView: () => { - if (__DEV__) { - console.warn( - 'One of your renderer is attempting to use WebView component, which has not been ' + - "provided as a prop to the RenderHtml component. As a consequence, the element won't be rendered." - ); - } - return null; - }, - defaultWebViewProps: {}, - renderersProps: {}, - setMarkersForTNode: () => null -}; - -const SharedPropsContext = React.createContext>( - defaultSharedPropsContext -); - -export function useSharedProps< - RendererProps extends Record = Record ->() { - return React.useContext(SharedPropsContext) as Required< - RenderHTMLSharedProps - >; -} - -export function useRendererProps< - RendererProps extends Record = Record, - K extends keyof RendererProps = any ->(k: K) { - return useSharedProps().renderersProps[k]; -} - -export function useDefaultContainerProps(): Pick< - TRendererBaseProps, - 'viewProps' | 'textProps' -> { - const sharedProps = useSharedProps(); - return { - viewProps: { - ...defaultSharedPropsContext.defaultViewProps, - ...sharedProps.defaultViewProps - }, - textProps: { - ...defaultSharedPropsContext.defaultTextProps, - ...sharedProps.defaultTextProps - } - }; -} -export function useDefaultTextProps(): TextProps { - return { - ...defaultSharedPropsContext.defaultTextProps, - ...useSharedProps().defaultTextProps - }; -} - -export function useDefaultViewProps(): ViewProps { - return { - ...defaultSharedPropsContext.defaultViewProps, - ...useSharedProps().defaultViewProps - }; -} - -export function useComputeMaxWidthForTag(tagName: string) { - const { computeEmbeddedMaxWidth } = useSharedProps(); - return useCallback( - (cw: number) => { - return computeEmbeddedMaxWidth(cw, tagName); - }, - [computeEmbeddedMaxWidth, tagName] - ); -} - -export default SharedPropsContext; diff --git a/packages/render-html/src/context/SharedPropsProvider.tsx b/packages/render-html/src/context/SharedPropsProvider.tsx new file mode 100644 index 000000000..07471c3f2 --- /dev/null +++ b/packages/render-html/src/context/SharedPropsProvider.tsx @@ -0,0 +1,83 @@ +import React, { PropsWithChildren, useCallback, useMemo } from 'react'; +import { TextProps, ViewProps } from 'react-native'; +import selectSharedProps from '../helpers/selectSharedProps'; +import { + RenderHTMLFragmentProps, + RenderHTMLSharedProps, + TRendererBaseProps +} from '../shared-types'; +import defaultSharedProps from './defaultSharedProps'; + +const SharedPropsContext = React.createContext>( + defaultSharedProps +); + +export function useSharedProps< + RendererProps extends Record = Record +>() { + return React.useContext(SharedPropsContext) as Required< + RenderHTMLSharedProps + >; +} + +export function useRendererProps< + RendererProps extends Record = Record, + K extends keyof RendererProps = any +>(k: K) { + return useSharedProps().renderersProps[k]; +} + +export function useDefaultContainerProps(): Pick< + TRendererBaseProps, + 'viewProps' | 'textProps' +> { + const sharedProps = useSharedProps(); + return { + viewProps: { + ...defaultSharedProps.defaultViewProps, + ...sharedProps.defaultViewProps + }, + textProps: { + ...defaultSharedProps.defaultTextProps, + ...sharedProps.defaultTextProps + } + }; +} +export function useDefaultTextProps(): TextProps { + return { + ...defaultSharedProps.defaultTextProps, + ...useSharedProps().defaultTextProps + }; +} + +export function useDefaultViewProps(): ViewProps { + return { + ...defaultSharedProps.defaultViewProps, + ...useSharedProps().defaultViewProps + }; +} + +export function useComputeMaxWidthForTag(tagName: string) { + const { computeEmbeddedMaxWidth } = useSharedProps(); + return useCallback( + (cw: number) => { + return computeEmbeddedMaxWidth(cw, tagName); + }, + [computeEmbeddedMaxWidth, tagName] + ); +} + +export default function SharedPropsProvider( + props: PropsWithChildren +) { + const memoizedSharedProps = useMemo( + () => selectSharedProps(props), + // eslint-disable-next-line react-hooks/exhaustive-deps + Object.values(selectSharedProps(props)) + ); + return React.createElement( + SharedPropsContext.Provider, + { value: memoizedSharedProps }, + props.children + ); +} diff --git a/packages/render-html/src/context/defaultSharedProps.ts b/packages/render-html/src/context/defaultSharedProps.ts new file mode 100644 index 000000000..6868f0559 --- /dev/null +++ b/packages/render-html/src/context/defaultSharedProps.ts @@ -0,0 +1,34 @@ +import { Dimensions, Linking } from 'react-native'; +import { RenderHTMLSharedProps } from '../shared-types'; + +const defaultSharedProps: Required = { + debug: false, + contentWidth: Dimensions.get('window').width, + enableExperimentalPercentWidth: false, + defaultTextProps: { + selectable: false, + allowFontScaling: true + }, + defaultViewProps: {}, + enableExperimentalMarginCollapsing: false, + computeEmbeddedMaxWidth: (contentWidth) => contentWidth, + imagesInitialDimensions: { + height: 50, + width: 50 + }, + onLinkPress: (_e, href) => Linking.canOpenURL(href) && Linking.openURL(href), + WebView: () => { + if (__DEV__) { + console.warn( + 'One of your renderer is attempting to use WebView component, which has not been ' + + "provided as a prop to the RenderHtml component. As a consequence, the element won't be rendered." + ); + } + return null; + }, + defaultWebViewProps: {}, + renderersProps: {}, + setMarkersForTNode: () => null +}; + +export default defaultSharedProps; diff --git a/packages/render-html/src/helpers/selectSharedProps.ts b/packages/render-html/src/helpers/selectSharedProps.ts index 857ed69af..b081638d4 100644 --- a/packages/render-html/src/helpers/selectSharedProps.ts +++ b/packages/render-html/src/helpers/selectSharedProps.ts @@ -3,14 +3,14 @@ import pick from 'ramda/src/pick'; import pipe from 'ramda/src/pipe'; import mergeRight from 'ramda/src/mergeRight'; import { RenderHTMLSharedProps, RenderHTMLProps } from '../shared-types'; -import { defaultSharedPropsContext } from '../context/SharedPropsContext'; +import defaultSharedProps from '../context/defaultSharedProps'; const selectSharedProps: ( props: Partial ) => Required = pipe( - pick(Object.keys(defaultSharedPropsContext)), + pick(Object.keys(defaultSharedProps)), pickBy((val) => val != null), - mergeRight(defaultSharedPropsContext) as any + mergeRight(defaultSharedProps) as any ); export default selectSharedProps; diff --git a/packages/render-html/src/hooks/useAssembledCommonProps.ts b/packages/render-html/src/hooks/useAssembledCommonProps.ts index 351a20aeb..d8341dbc7 100644 --- a/packages/render-html/src/hooks/useAssembledCommonProps.ts +++ b/packages/render-html/src/hooks/useAssembledCommonProps.ts @@ -14,7 +14,7 @@ import { } from '../shared-types'; import mergeCollapsedMargins from '../helpers/mergeCollapsedMargins'; import { useRendererConfig } from '../context/RenderRegistryProvider'; -import { useDefaultContainerProps } from '../context/SharedPropsContext'; +import { useDefaultContainerProps } from '../context/SharedPropsProvider'; function getStylesForTnode(tnode: T): NativeStyleProp { if (tnode instanceof TBlock) { diff --git a/packages/render-html/src/index.ts b/packages/render-html/src/index.ts index 6b7ebb438..effc93b51 100644 --- a/packages/render-html/src/index.ts +++ b/packages/render-html/src/index.ts @@ -69,7 +69,7 @@ export { useComputeMaxWidthForTag, useRendererProps, useSharedProps -} from './context/SharedPropsContext'; +} from './context/SharedPropsProvider'; export { useDocumentMetadata } from './context/DocumentMetadataProvider'; export { default as domNodeToHTMLString } from './helpers/domNodeToHTMLString'; export type { DomNodeToHtmlReporter } from './helpers/domNodeToHTMLString'; diff --git a/packages/render-html/src/renderers/IMGRenderer.tsx b/packages/render-html/src/renderers/IMGRenderer.tsx index 7293f9d4b..57a176ee5 100644 --- a/packages/render-html/src/renderers/IMGRenderer.tsx +++ b/packages/render-html/src/renderers/IMGRenderer.tsx @@ -2,7 +2,7 @@ import React, { ClassAttributes } from 'react'; import { TBlock } from '@native-html/transient-render-engine'; import IMGElement, { IMGElementProps } from '../elements/IMGElement'; import { DefaultBlockRenderer } from '../render/render-types'; -import { useComputeMaxWidthForTag } from '../context/SharedPropsContext'; +import { useComputeMaxWidthForTag } from '../context/SharedPropsProvider'; import { ImageStyle } from 'react-native'; import { defaultHTMLElementModels } from '@native-html/transient-render-engine'; import { DefaultTagRendererProps } from '../shared-types'; diff --git a/packages/render-html/src/shared-types.ts b/packages/render-html/src/shared-types.ts index 2073ea02a..06109fa2d 100644 --- a/packages/render-html/src/shared-types.ts +++ b/packages/render-html/src/shared-types.ts @@ -389,7 +389,7 @@ export interface SourceLoaderProps RenderHTMLProps, 'source' | 'remoteLoadingView' | 'remoteErrorView' | 'onHTMLLoaded' > { - children: (resource: ResolvedResourceProps) => ReactElement; + children: (resource: ResolvedResourceProps) => any; } export interface FallbackFontsDefinitions {