fix(react): first render performance improvements

This commit is contained in:
Ely Lucas
2019-12-09 14:36:47 -07:00
committed by GitHub
parent 684293ddbf
commit 1c7d1e5cf1
11 changed files with 160 additions and 174 deletions

View File

@ -176,34 +176,30 @@ will match the following tab:
### React
```tsx
import React from 'react';
import { IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel, IonBadge } from '@ionic/react';
export const TabsExample: React.FC = () => (
<IonTabs>
<IonRouterOutlet>
<Redirect exact path="/" to="/tabs/schedule" />
{/*
Using the render method prop cuts down the number of renders your components will have due to route changes.
Use the component prop when your component depends on the RouterComponentProps passed in automatically.
*/}
<Route path="/tabs/schedule" render={() => <SchedulePage />} exact={true} />
<Route path="/tabs/speakers" render={() => <SpeakerList />} exact={true} />
<Route path="/tabs/map" render={() => <MapView />} exact={true} />
<Route path="/tabs/about" render={() => <About />} exact={true} />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="schedule" href="/tabs/schedule">
<IonIcon icon={calendar} />
<IonTabButton tab="schedule">
<IonIcon name="calendar" />
<IonLabel>Schedule</IonLabel>
<IonBadge>6</IonBadge>
</IonTabButton>
<IonTabButton tab="speakers" href="/tabs/speakers">
<IonIcon icon={contacts} />
<IonTabButton tab="speakers">
<IonIcon name="contacts" />
<IonLabel>Speakers</IonLabel>
</IonTabButton>
<IonTabButton tab="map" href="/tabs/map">
<IonIcon icon={map} />
<IonTabButton tab="map">
<IonIcon name="map" />
<IonLabel>Map</IonLabel>
</IonTabButton>
<IonTabButton tab="about" href="/tabs/about">
<IonIcon icon={informationCircle} />
<IonTabButton tab="about">
<IonIcon name="information-circle" />
<IonLabel>About</IonLabel>
</IonTabButton>
</IonTabBar>

View File

@ -0,0 +1 @@
export type IonRouteAction = 'push' | 'replace' | 'pop';

View File

@ -4,11 +4,12 @@ import { Location as HistoryLocation, UnregisterCallback } from 'history';
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { IonRouteAction } from './IonRouteAction';
import { StackManager } from './StackManager';
interface NavManagerProps extends RouteComponentProps {
onNavigateBack: (defaultHref?: string) => void;
onNavigate: (type: 'push' | 'replace', path: string, state?: any) => void;
onNavigate: (type: 'push' | 'replace' | 'pop', path: string, state?: any) => void;
}
export class NavManager extends React.Component<NavManagerProps, NavContextState> {
@ -24,7 +25,7 @@ export class NavManager extends React.Component<NavManagerProps, NavContextState
getStackManager: this.getStackManager.bind(this),
getPageManager: this.getPageManager.bind(this),
currentPath: this.props.location.pathname,
registerIonPage: () => { return; }, // overridden in View for each IonPage
registerIonPage: () => { return; } // overridden in View for each IonPage
};
this.listenUnregisterCallback = this.props.history.listen((location: HistoryLocation) => {
@ -52,8 +53,8 @@ export class NavManager extends React.Component<NavManagerProps, NavContextState
this.props.onNavigateBack(defaultHref);
}
navigate(path: string, direction?: RouterDirection | 'none', type: 'push' | 'replace' = 'push') {
this.props.onNavigate(type, path, direction);
navigate(path: string, direction?: RouterDirection | 'none', ionRouteAction: IonRouteAction = 'push') {
this.props.onNavigate(ionRouteAction, path, direction);
}
getPageManager() {

View File

@ -7,6 +7,7 @@ import { RouteComponentProps, matchPath, withRouter } from 'react-router-dom';
import { generateId, isDevMode } from '../utils';
import { LocationHistory } from '../utils/LocationHistory';
import { IonRouteAction } from './IonRouteAction';
import { IonRouteData } from './IonRouteData';
import { NavManager } from './NavManager';
import { RouteManagerContext, RouteManagerContextState } from './RouteManagerContext';
@ -15,15 +16,19 @@ import { ViewStack, ViewStacks } from './ViewStacks';
interface RouteManagerState extends RouteManagerContextState {
location?: HistoryLocation;
action?: HistoryAction;
action?: IonRouteAction;
}
class RouteManager extends React.Component<RouteComponentProps, RouteManagerState> {
listenUnregisterCallback: UnregisterCallback | undefined;
activeIonPageId?: string;
currentDirection?: RouterDirection;
currentIonRouteAction?: IonRouteAction;
currentRouteDirection?: RouterDirection;
locationHistory = new LocationHistory();
routes: { [key: string]: any; } = {};
routes: { [key: string]: React.ReactElement<any>; } = {};
ionPageElements: { [key: string]: HTMLElement; } = {};
routerOutlets: { [key: string]: HTMLIonRouterOutletElement; } = {};
firstRender = true;
constructor(props: RouteComponentProps) {
super(props);
@ -52,7 +57,8 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
componentDidUpdate(_prevProps: RouteComponentProps, prevState: RouteManagerState) {
// Trigger a page change if the location or action is different
if (this.state.location && prevState.location !== this.state.location || prevState.action !== this.state.action) {
this.setActiveView(this.state.location!, this.state.action!);
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
this.setActiveView(this.state.location!, this.state.action!, viewStacks);
}
}
@ -71,10 +77,10 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
const { view } = viewStacks.findViewInfoById(viewId);
if (view) {
view.show = false;
view.ionPageElement = undefined;
view.isIonRoute = false;
view.prevId = undefined;
view.key = generateId();
delete this.ionPageElements[view.id];
this.setState({
viewStacks
});
@ -82,23 +88,29 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
}
historyChange(location: HistoryLocation, action: HistoryAction) {
location.state = location.state || { direction: this.currentDirection };
this.currentDirection = undefined;
if (action === 'PUSH') {
const ionRouteAction = this.currentIonRouteAction === 'pop' ? 'pop' : action.toLowerCase();
let direction = this.currentRouteDirection;
if (ionRouteAction === 'push') {
this.locationHistory.add(location);
} else if ((action === 'REPLACE' && location.state.direction === 'back') || action === 'POP') {
} else if (ionRouteAction === 'pop') {
this.locationHistory.pop();
} else {
direction = direction || 'back';
} else if (ionRouteAction === 'replace') {
this.locationHistory.replace(location);
}
this.setState({
location,
action
});
direction = 'none';
}
setActiveView(location: HistoryLocation, action: HistoryAction) {
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
location.state = location.state || { direction };
this.setState({
location,
action: ionRouteAction as IonRouteAction
});
this.currentRouteDirection = undefined;
this.currentIonRouteAction = undefined;
}
setActiveView(location: HistoryLocation, action: IonRouteAction, viewStacks: ViewStacks) {
let direction: RouterDirection | undefined = (location.state && location.state.direction) || 'forward';
let leavingView: ViewItem | undefined;
const viewStackKeys = viewStacks.getKeys();
@ -122,25 +134,18 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
this.activeIonPageId = enteringView.id;
if (leavingView) {
if (direction === 'forward') {
if (action === 'PUSH') {
if (action === 'push' && direction === 'forward') {
/**
* If the page is being pushed into the stack by another view,
* record the view that originally directed to the new view for back button purposes.
*/
enteringView.prevId = leavingView.id;
} else if (action === 'POP') {
direction = leavingView.prevId === enteringView.id ? 'back' : 'none';
} else {
direction = direction || 'back';
leavingView.mount = false;
}
}
if (direction === 'back' || action === 'REPLACE') {
} else if (action === 'pop' || action === 'replace') {
leavingView.mount = false;
this.removeOrphanedViews(enteringView, enteringViewStack);
}
leavingViewHtml = enteringView.id === leavingView.id ? leavingView.ionPageElement!.outerHTML : undefined;
leavingViewHtml = enteringView.id === leavingView.id ? this.ionPageElements[leavingView.id].outerHTML : undefined;
} else {
// If there is not a leavingView, then we shouldn't provide a direction
direction = undefined;
@ -167,16 +172,17 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
if (shouldTransitionPage) {
const { view: enteringView, viewStack } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
if (enteringView && viewStack) {
const enteringEl = enteringView.ionPageElement ? enteringView.ionPageElement : undefined;
const leavingEl = leavingView && leavingView.ionPageElement ? leavingView.ionPageElement : undefined;
const enteringEl = this.ionPageElements[enteringView.id];
const leavingEl = leavingView && this.ionPageElements[leavingView.id];
if (enteringEl) {
// Don't animate from an empty view
const navDirection = leavingEl && leavingEl.innerHTML === '' ? undefined : direction === 'none' ? undefined : direction;
const shouldGoBack = !!enteringView.prevId;
const routerOutlet = this.routerOutlets[viewStack.id];
this.commitView(
enteringEl!,
leavingEl!,
viewStack.routerOutlet,
routerOutlet,
navDirection,
shouldGoBack,
leavingViewHtml);
@ -211,12 +217,13 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
this.removeOrphanedViews(v, viewStack);
// If view is not currently visible, go ahead and remove it from DOM
if (v.ionPageElement!.classList.contains('ion-page-hidden')) {
const page = this.ionPageElements[v.id];
if (page.classList.contains('ion-page-hidden')) {
v.show = false;
v.ionPageElement = undefined;
v.isIonRoute = false;
v.prevId = undefined;
v.key = generateId();
delete this.ionPageElements[v.id];
}
v.mount = false;
}
@ -270,9 +277,9 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
const prevViewStacks = Object.assign(new ViewStacks(), prevState.viewStacks);
const newStack: ViewStack = {
id: stack,
views: stackItems,
routerOutlet
views: stackItems
};
this.routerOutlets[stack] = routerOutlet;
if (activeId) {
this.activeIonPageId = activeId;
}
@ -286,18 +293,6 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
}
async setupRouterOutlet(routerOutlet: HTMLIonRouterOutletElement) {
const waitUntilReady = async () => {
if (routerOutlet.componentOnReady) {
routerOutlet.dispatchEvent(new Event('routerOutletReady'));
return;
} else {
setTimeout(() => {
waitUntilReady();
}, 0);
}
};
await waitUntilReady();
const canStart = () => {
const config = getConfig();
@ -329,20 +324,13 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
}
syncView(page: HTMLElement, viewId: string) {
this.setState(state => {
const viewStacks = Object.assign(new ViewStacks(), state.viewStacks);
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
const { view } = viewStacks.findViewInfoById(viewId);
view!.ionPageElement = page;
view!.isIonRoute = true;
return {
viewStacks
};
}, () => {
this.setActiveView(this.state.location || this.props.location, this.state.action!);
});
if (view) {
view.isIonRoute = true;
this.ionPageElements[view.id] = page;
this.setActiveView(this.state.location || this.props.location, this.state.action!, viewStacks);
}
}
syncRoute(_id: string, routerOutlet: any) {
@ -359,6 +347,11 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
}
private async commitView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOutlet: HTMLIonRouterOutletElement, direction?: NavDirection, showGoBack?: boolean, leavingViewHtml?: string) {
if (!this.firstRender) {
if (!('componentOnReady' in ionRouterOutlet)) {
await waitUntilRouterOutletReady(ionRouterOutlet);
}
if ((enteringEl === leavingEl) && direction && leavingViewHtml) {
// If a page is transitioning to another version of itself
@ -388,14 +381,28 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
leavingEl.classList.add('ion-page-hidden');
leavingEl.setAttribute('aria-hidden', 'true');
}
} else {
enteringEl.classList.remove('ion-page-invisible');
enteringEl.style.zIndex = '101';
this.firstRender = false;
}
}
handleNavigate(type: 'push' | 'replace', path: string, direction?: RouterDirection) {
this.currentDirection = direction;
if (type === 'push') {
handleNavigate(ionRouteAction: IonRouteAction, path: string, direction?: RouterDirection) {
this.currentIonRouteAction = ionRouteAction;
switch (ionRouteAction) {
case 'push':
this.currentRouteDirection = direction;
this.props.history.push(path);
} else {
break;
case 'pop':
this.currentRouteDirection = direction || 'back';
this.props.history.replace(path);
break;
case 'replace':
this.currentRouteDirection = 'none';
this.props.history.replace(path);
break;
}
}
@ -405,26 +412,26 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
if (leavingView.id === leavingView.prevId) {
const previousLocation = this.locationHistory.previous();
if (previousLocation) {
this.handleNavigate('replace', previousLocation.pathname + previousLocation.search, 'back');
this.handleNavigate('pop', previousLocation.pathname + previousLocation.search);
} else {
defaultHref && this.handleNavigate('replace', defaultHref, 'back');
defaultHref && this.handleNavigate('pop', defaultHref);
}
} else {
const { view: enteringView } = this.state.viewStacks.findViewInfoById(leavingView.prevId);
if (enteringView) {
const lastLocation = this.locationHistory.findLastLocationByUrl(enteringView.routeData.match!.url);
if (lastLocation) {
this.handleNavigate('replace', lastLocation.pathname + lastLocation.search, 'back');
this.handleNavigate('pop', lastLocation.pathname + lastLocation.search);
} else {
this.handleNavigate('replace', enteringView.routeData.match!.url, 'back');
this.handleNavigate('pop', enteringView.routeData.match!.url);
}
} else {
const currentLocation = this.locationHistory.previous();
if (currentLocation) {
this.handleNavigate('replace', currentLocation.pathname + currentLocation.search, 'back');
this.handleNavigate('pop', currentLocation.pathname + currentLocation.search);
} else {
if (defaultHref) {
this.handleNavigate('replace', defaultHref, 'back');
this.handleNavigate('pop', defaultHref);
}
}
}
@ -464,5 +471,15 @@ function clonePageElement(leavingViewHtml: string) {
return newEl.firstChild as HTMLElement;
}
async function waitUntilRouterOutletReady(ionRouterOutlet: HTMLIonRouterElement) {
if ('componentOnReady' in ionRouterOutlet) {
return;
} else {
setTimeout(() => {
waitUntilRouterOutletReady(ionRouterOutlet);
}, 0);
}
}
export const RouteManagerWithRouter = withRouter(RouteManager);
RouteManagerWithRouter.displayName = 'RouteManager';

View File

@ -13,13 +13,10 @@ interface StackManagerProps {
children?: React.ReactNode;
}
interface StackManagerState {
routerOutletReady: boolean;
}
interface StackManagerState { }
class StackManagerInner extends React.Component<StackManagerProps, StackManagerState> {
routerOutletEl: React.RefObject<HTMLIonRouterOutletElement> = React.createRef();
id: string;
constructor(props: StackManagerProps) {
@ -27,18 +24,11 @@ class StackManagerInner extends React.Component<StackManagerProps, StackManagerS
this.id = this.props.id || generateId();
this.handleViewSync = this.handleViewSync.bind(this);
this.handleHideView = this.handleHideView.bind(this);
this.state = {
routerOutletReady: false
};
this.state = {};
}
componentDidMount() {
this.props.routeManager.setupIonRouter(this.id, this.props.children, this.routerOutletEl.current!);
this.routerOutletEl.current!.addEventListener('routerOutletReady', () => {
this.setState({
routerOutletReady: true
});
});
}
static getDerivedStateFromProps(props: StackManagerProps, state: StackManagerState) {
@ -70,9 +60,7 @@ class StackManagerInner extends React.Component<StackManagerProps, StackManagerS
const viewStack = routeManager.viewStacks.get(this.id);
const views = (viewStack || { views: [] }).views.filter(x => x.show);
const ionRouterOutlet = React.Children.only(this.props.children) as React.ReactElement;
const { routerOutletReady } = this.state;
const childElements = routerOutletReady ? views.map(view => {
const childElements = views.map(view => {
const route = routeManager.getRoute(view.routeId);
return (
<ViewTransitionManager
@ -90,7 +78,7 @@ class StackManagerInner extends React.Component<StackManagerProps, StackManagerS
</View>
</ViewTransitionManager>
);
}) : <div></div>;
});
const elementProps: any = {
ref: this.routerOutletEl

View File

@ -1,6 +1,5 @@
import { IonLifeCycleContext, NavContext } from '@ionic/react';
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import { isDevMode } from '../utils';
@ -20,20 +19,6 @@ export class View extends React.Component<ViewProps, {}> {
context!: React.ContextType<typeof IonLifeCycleContext>;
ionPage?: HTMLElement;
componentDidMount() {
/**
* If we can tell if view is a redirect, hide it so it will work again in future
*/
const { view, route } = this.props;
if (route.type === Redirect) {
this.props.onHideView(view.id);
} else if (route.type === Route && route.props.render) {
if (route.props.render().type === Redirect) {
this.props.onHideView(view.id);
}
}
}
componentWillUnmount() {
if (this.ionPage) {
this.ionPage.removeEventListener('ionViewWillEnter', this.ionViewWillEnterHandler.bind(this));

View File

@ -5,10 +5,7 @@ export interface ViewItem<RouteData = any> {
key: string;
routeId: string;
/** The <Route /> or <Redirect /> component associated with the view */
// route: React.ReactElement<any>;
/** The reference to the <IonPage /> element. */
ionPageElement?: HTMLElement;
/** The routeData for the view. */
routeData: RouteData;
/** Used to track which page pushed the page into view. Used for back button purposes. */

View File

@ -6,7 +6,6 @@ import { ViewItem } from './ViewItem';
export interface ViewStack {
id: string;
routerOutlet: HTMLIonRouterOutletElement;
views: ViewItem[];
}

View File

@ -30,6 +30,8 @@
"jsx-wrap-multiline": false,
"forin": false,
"strict-type-predicates": false,
"no-unused-expression": false
"no-unused-expression": false,
"no-constant-condition": false,
"no-empty-interface": false
}
}

View File

@ -75,7 +75,7 @@ const IonTabBarUnwrapped = /*@__PURE__*/(() => class extends React.Component<Pro
if (originalHref === currentHref) {
this.context.navigate(originalHref, 'none');
} else {
this.context.navigate(originalHref, 'back', 'replace');
this.context.navigate(originalHref, 'back', 'pop');
}
} else {
if (this.props.onIonTabsWillChange) {

View File

@ -5,7 +5,7 @@ export interface NavContextState {
getPageManager: () => any;
getStackManager: () => any;
goBack: (defaultHref?: string) => void;
navigate: (path: string, direction?: RouterDirection | 'none', type?: 'push' | 'replace') => void;
navigate: (path: string, direction?: RouterDirection | 'none', ionRouteAction?: 'push' | 'replace' | 'pop') => void;
hasIonicRouter: () => boolean;
registerIonPage: (page: HTMLElement) => void;
currentPath: string | undefined;