Skip to content
This repository was archived by the owner on Jan 11, 2023. It is now read-only.

[Work in progress] Add reselect and reduce unnecessary renders#2784

Merged
jasonLaster merged 16 commits into
firefox-devtools:masterfrom
asolove:reselect2
May 3, 2017
Merged

[Work in progress] Add reselect and reduce unnecessary renders#2784
jasonLaster merged 16 commits into
firefox-devtools:masterfrom
asolove:reselect2

Conversation

@asolove

@asolove asolove commented May 2, 2017

Copy link
Copy Markdown
Contributor

Associated Issue: #2727

Background

In issue #2727, @fflorent reported a very severe performance problem on pages with lots of script tags. In the course of debugging that problem, we noticed that all the components connected to the redux store were being re-rendered on every state change, even if the state change didn't effect the data that component read. That was odd, because Redux promises not to re-render connected components if the props don't change (as tested by shallow equality). The culprit was our selector functions, which often use map and filter to build up results. Even if the underlying data stayed the same, these selectors return new objects and arrays every time they are called. A common solution to this problem is to use memoized selectors, which promise to return the same objects if the underlying data hasn't changed. One popular library for doing this is reselect.

So the goal of this PR is to make components only re-render when their underlying data has changed, by memoizing selectors as necessary.

Summary of Changes

For each component, make it a PureComponent and ensure its selectors don't change if their underlying data doesn't change:

  • Frames
  • Breakpoints
  • Scopes
  • Expressions
  • Editor
  • Tabs
  • App

Test Plan

All existing tests should pass (no functionality should have changed).

To verify that re-renders have decreased:

  • Do a development build so React Perf tools are installed.
  • Open debugger.html and then open a debugger for it.
  • Type Perf.start()
  • Interact with the app as much as you like...
  • Type Perf.stop()
  • Perf.printExclusive() will now print a table with how long it spend re-rendering each component. This allows you to see if one component is changing because of unrelated data.

@codecov

codecov Bot commented May 2, 2017

Copy link
Copy Markdown

Codecov Report

Merging #2784 into master will increase coverage by 0.27%.
The diff coverage is 79.54%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #2784      +/-   ##
==========================================
+ Coverage   58.74%   59.01%   +0.27%     
==========================================
  Files          59       59              
  Lines        2288     2301      +13     
  Branches      469      470       +1     
==========================================
+ Hits         1344     1358      +14     
+ Misses        944      943       -1
Impacted Files Coverage Δ
src/selectors.js 100% <ø> (ø) ⬆️
src/utils/source.js 50% <0%> (-2.39%) ⬇️
src/reducers/expressions.js 97.95% <100%> (+0.04%) ⬆️
src/reducers/pause.js 21.91% <75%> (+7.83%) ⬆️
src/reducers/sources.js 78.78% <87.5%> (+0.56%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c00a0a4...9ff5d29. Read the comment docs.

@fflorent

fflorent commented May 2, 2017

Copy link
Copy Markdown

Thanks @asolove! I may give it a try tomorrow.

Florent

@fflorent

fflorent commented May 2, 2017

Copy link
Copy Markdown

BTW, I see you use babel. Doesn't it introduce performance issues? (especially for Maps and Sets)

Florent

@jasonLaster jasonLaster left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I like where this is going. I'll review more closely soon

Comment thread src/main.js Outdated
if (process.env.NODE_ENV !== "production") {
const Perf = require("react-addons-perf");
window.Perf = Perf;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👍

Comment thread src/reducers/pause.js Outdated
export function getPause(state: OuterState) {
return state.pause.get("pause");
}
const getPauseWrapper = state => state.pause;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I like this style

Comment thread src/reducers/sources.js Outdated
// (right now) to type those wrapped functions.
type OuterState = { sources: Record<SourcesState> };

const getSourcesWrapper = state => state.sources;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

perhaps sourcesState or is that too opaque?

}
const _getBreakpoints = createSelector(
getBreakpoints,
state => state,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

does this negate the selector?

Comment thread src/reducers/sources.js Outdated
return state.sources.sources.find(source => source.get("url") === url);
}

export function getSourceInSources(sources: I.Map<string, Source>, id: string) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: getSourceByID

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Well the problem is we already have a selector getSourceById that takes the full state and an id. This function takes just the actual sources map and an id. For various kinds of callers, we need both.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah - the underscore was meh when it was a private function, but below it's public... so i don't love it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, not sure what to do without finding a better name for either the top-level "sources state" or the nested "sources by id map".

Comment thread src/reducers/sources.js Outdated
@@ -284,8 +284,12 @@ export function getSourceByURL(state: OuterState, url: string) {
return state.sources.sources.find(source => source.get("url") === url);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we should have a separate URL to ID mapping

@jasonLaster jasonLaster merged commit cdcb10b into firefox-devtools:master May 3, 2017
@fflorent

fflorent commented May 3, 2017

Copy link
Copy Markdown

@asolove I heard you also recently made a patch that is not landed yet in Firefox Dev Edition and fixed a big performance issue. And at work, I could notice a really good improvement of the performance already in master using the repository. Thanks a lot for this big effort! :)

Florent

@jasonLaster

Copy link
Copy Markdown
Contributor

@fflorent babel certainly adds a slow down, but it is worth it to have some consistency across runtimes. We don't use it to add experimental language features.

DanUgelow pushed a commit to DanUgelow/debugger.html that referenced this pull request May 4, 2017
Memoizes selectors to prevent unnecessary renders.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants