Last active
April 23, 2017 21:26
-
-
Save gpeal/a77551b6548300ff36a764e0db01bf31 to your computer and use it in GitHub Desktop.
Airbnb React Native findOffscreenViews (written by Leland Richardson)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* eslint-disable no-underscore-dangle, no-param-reassign, no-restricted-syntax */ | |
| import { UIManager, findNodeHandle } from 'react-native'; | |
| const name = fn => fn.displayName || fn.name || 'UnknownComponent'; | |
| const nodeName = (component) => name(component._currentElement.type); | |
| const isHost = component => !!component._renderedChildren; | |
| const isText = component => typeof component._stringText === 'string'; | |
| const { hasOwnProperty } = Object.prototype; | |
| // This makes a node with all of the layout and react information we need | |
| function makeLayoutNode(component, parent) { | |
| const text = isText(component); | |
| return { | |
| parent, | |
| component, | |
| name: text ? '"' : nodeName(component), | |
| index: -1, | |
| isHost: isHost(component), | |
| isText: text, | |
| detached: false, | |
| resolved: false, | |
| x: -1, | |
| y: -1, | |
| width: -1, | |
| height: -1, | |
| children: null, | |
| }; | |
| } | |
| function buildLayoutTreeOfRoot(component) { | |
| const promises = []; | |
| const root = makeLayoutNode(component, null); | |
| // this call will build a tree of all react views and their current layout, but it's async, | |
| // so we stuff all of the promises into an array and use Promise.all to make sure we have | |
| // all the info that we need. | |
| buildLayoutTree(component, root, promises); | |
| return Promise.all(promises).then(() => root); | |
| } | |
| function buildLayoutTree(component, node, promises) { | |
| if (!component) return; | |
| if (isText(component)) return; | |
| if (component._instance) { | |
| UIManager.measureInWindow( | |
| findNodeHandle(component._instance), | |
| measureHandler(node, promises) | |
| ); | |
| } | |
| if (component._renderedChildren) { | |
| if (node && node.parent && node.parent.component && node.parent.component._instance) { | |
| // host nodes don't have an instance to measure (or at least i don't know how to find it), | |
| // so we just measure their parent and use those values instead | |
| UIManager.measureInWindow( | |
| findNodeHandle(node.parent.component._instance), | |
| measureHandler(node, promises) | |
| ); | |
| } | |
| const children = component._renderedChildren; | |
| node.children = []; | |
| for (const key in children) { | |
| if (hasOwnProperty.call(children, key)) { | |
| const child = children[key]; | |
| const childNode = makeLayoutNode(child, node); | |
| childNode.index = node.children.length; | |
| node.children.push(childNode); | |
| buildLayoutTree(child, childNode, promises); | |
| } | |
| } | |
| } else if (component._renderedComponent) { | |
| let child = component._renderedComponent; | |
| while (child._currentElement === null && child._renderedComponent !== null) { | |
| child = child._renderedComponent; | |
| } | |
| const childNode = makeLayoutNode(child, node); | |
| node.children = childNode; | |
| buildLayoutTree(child, childNode, promises); | |
| } | |
| } | |
| function measureHandler(node, promises) { | |
| let resolve; | |
| promises.push(new Promise((r) => { resolve = r; })); | |
| return (x, y, width, height) => { | |
| node.x = x; | |
| node.y = y; | |
| node.width = width; | |
| node.height = height; | |
| node.resolved = true; | |
| node.detached = x === 0 && y === 0 && width === 0 && height === 0; | |
| resolve(); | |
| }; | |
| } | |
| module.exports = buildLayoutTreeOfRoot; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* eslint-disable no-underscore-dangle, no-param-reassign, no-restricted-syntax */ | |
| import { Dimensions } from 'react-native'; | |
| import buildLayoutTree from './buildLayoutTree'; | |
| import getReactRoot from './getReactRoot'; | |
| const WINDOW_HEIGHT = Dimensions.get('window').height; | |
| const PROPS_CHAR_LIMIT = 30; | |
| const COMPONENT_COLOR = 'color: #B2349C;'; | |
| const PROPS_COLOR = 'color: #A5672A;'; | |
| const WARNING_COLOR = 'color: red;'; | |
| const METADATA_COLOR = 'color: #aaa;'; | |
| const print = (n, s) => `${Array.from({ length: n + 1 }).join(' ')}${s}\n`; | |
| function findOffscreenViews() { | |
| const root = getReactRoot(); | |
| if (root === null) { | |
| console.log('No react views are currently rendered'); | |
| return; | |
| } | |
| findOffscreenViewsOfRoot(root); | |
| } | |
| function findOffscreenViewsOfRoot(component) { | |
| // flag that allows us to check if we didn't find anything so we can tell user that | |
| const flag = { found: false }; | |
| buildLayoutTree(component) | |
| .then((tree) => traverseTreeForOffscreenViews(tree, flag)) | |
| .then(() => { | |
| if (!flag.found) { | |
| console.log('Couldn\'t find any views that were rendered offscreen'); | |
| } | |
| }); | |
| } | |
| function traverseTreeForOffscreenViews(node, flag) { | |
| if (node.children === null) { | |
| // continue | |
| } else if (Array.isArray(node.children)) { | |
| const offscreenChildren = node.children.filter(isOffscreen); | |
| // number of views (including subviews) offscreen | |
| const viewCount = node.children.reduce((sum, el) => { | |
| return isOffscreen(el) ? (sum + viewsInSubtree(el)) : sum; | |
| }, 0); | |
| if (offscreenChildren.length === node.children.length) { | |
| // all offscreen. don't traverse any further. | |
| } else { | |
| if (offscreenChildren.length > 0) { | |
| flag.found = true; | |
| prettyPrintNode(node, offscreenChildren, viewCount); | |
| } | |
| // only some children offscreen... we want to keep traversing to get more details into those | |
| // specific children | |
| node.children.forEach((el) => traverseTreeForOffscreenViews(el, flag)); | |
| } | |
| } else { | |
| traverseTreeForOffscreenViews(node.children, flag); | |
| } | |
| } | |
| function isOffscreen(node) { | |
| if (node.resolved) { | |
| return !node.detached && (node.y > WINDOW_HEIGHT || (node.y + node.height) < 0); | |
| } else if (node.children !== null && !Array.isArray(node.children)) { | |
| return isOffscreen(node.children); | |
| } | |
| return false; | |
| } | |
| function viewsInSubtree(node) { | |
| const stack = [node]; | |
| let n = 0; | |
| let el; | |
| while (stack.length !== 0) { | |
| el = stack.pop(); | |
| if (el.resolved && !el.detached) { | |
| n += 1; | |
| } | |
| if (el.children === null) { | |
| // continue | |
| } else if (Array.isArray(el.children)) { | |
| stack.push(...el.children); | |
| } else { | |
| stack.push(el.children); | |
| } | |
| } | |
| return n; | |
| } | |
| function ancestorsOfNode(node) { | |
| const nodes = []; | |
| let el = node; | |
| while (el !== null) { | |
| if (el.name === 'WrappedScreen') { | |
| break; // manually stop here since this is top of hierarchy that we care about... | |
| } | |
| // skip over these because they are so common, and always have `View` and `Text` as parents | |
| if (el.name !== 'RCTView' && el.name !== 'RCTText' && el.name !== '"') { | |
| nodes.push(el); | |
| } | |
| el = el.parent; | |
| } | |
| return nodes.reverse(); | |
| } | |
| function prettySource(source) { | |
| const lastSlashIndex = source.fileName.lastIndexOf('/'); | |
| if (lastSlashIndex === -1) { | |
| return `${source.fileName}:${source.lineNumber}`; | |
| } | |
| const realFileName = source.fileName.slice(lastSlashIndex + 1); | |
| return `${realFileName}:${source.lineNumber}`; | |
| } | |
| function propString(key, prop) { | |
| switch (typeof prop) { | |
| case 'function': | |
| return `${key}=ƒ`; | |
| case 'string': | |
| return prop.length > 20 ? `${key}="..."` : `${key}="${prop}"`; | |
| case 'number': | |
| return `${key}=${prop}`; | |
| case 'boolean': | |
| return prop ? `${key}` : `${key}=false`; | |
| case 'object': | |
| return Array.isArray(prop) ? `${key}={[...]}` : `${key}={{...}}`; | |
| default: | |
| return `${key}={[${typeof prop}]}`; | |
| } | |
| } | |
| function propsString(props) { | |
| const keys = Object.keys(props); | |
| let result = ''; | |
| for (let i = 0; i < keys.length; i += 1) { | |
| const key = keys[i]; | |
| switch (key) { | |
| case 'children': break; | |
| case 'style': break; | |
| default: { | |
| const add = propString(key, props[key]); | |
| if (result.length + add.length > PROPS_CHAR_LIMIT) { | |
| result += ' ...'; | |
| return result; | |
| } | |
| result += ` ${add}`; | |
| } break; | |
| } | |
| } | |
| return result; | |
| } | |
| function prettyPrintNode(node, offscreenChildren, offscreenViews) { | |
| const ancestors = ancestorsOfNode(node); | |
| const buffer = { | |
| text: '', | |
| styles: [], | |
| }; | |
| console.groupCollapsed( | |
| `FOUND: %c${offscreenViews} %cattached offscreen views (from ${offscreenChildren.length} / ${node.children.length} children)`, | |
| 'color: red;', | |
| 'color: black;' | |
| ); | |
| let index = 0; | |
| while (index < ancestors.length) { | |
| const parent = ancestors[index]; | |
| const el = parent.component._currentElement; | |
| const source = el._source; | |
| const showIndex = parent.index !== -1 && parent.parent.children.length !== 1; | |
| const props = propsString(el.props); | |
| let post = ''; | |
| if (showIndex) { | |
| post += ` (child ${parent.index} of ${parent.parent.children.length})`; | |
| } | |
| if (source && source.fileName) { | |
| post += ` (${prettySource(source)})`; | |
| } | |
| buffer.text += print(index, `%c<${parent.name}%c${props}%c>%c${post}`); | |
| buffer.styles.push( | |
| COMPONENT_COLOR, | |
| PROPS_COLOR, | |
| COMPONENT_COLOR, | |
| METADATA_COLOR | |
| ); | |
| index += 1; | |
| } | |
| buffer.text += print(index, '%c<--'); | |
| buffer.text += print(index + 3, `${offscreenChildren.length} / ${node.children.length} children here are offscreen.`); | |
| buffer.text += print(index + 3, `This currently amounts to ${offscreenViews} total native views`); | |
| buffer.text += print(index + 3, 'You should consider setting `removeClippedSubviews` to true on the '); | |
| buffer.text += print(index + 3, 'on the containing View in order to improve scroll performance.'); | |
| buffer.text += print(index, '-->'); | |
| buffer.styles.push(WARNING_COLOR); | |
| const children = node.children; | |
| for (let i = 0; i < children.length; i += 1) { | |
| const child = children[i]; | |
| const el = child.component._currentElement; | |
| const source = el._source; | |
| let warning = ''; | |
| if (isOffscreen(child)) { | |
| warning = ' (OFFSCREEN!)'; | |
| } | |
| const props = propsString(el.props); | |
| let post = ''; | |
| if (source && source.fileName) { | |
| post += ` (${prettySource(source)})`; | |
| } | |
| buffer.text += print(index, `%c<${child.name}%c${props} %c/>%c${warning}%c${post}`); | |
| buffer.styles.push( | |
| COMPONENT_COLOR, | |
| PROPS_COLOR, | |
| COMPONENT_COLOR, | |
| WARNING_COLOR, | |
| METADATA_COLOR | |
| ); | |
| } | |
| while (index > 0) { | |
| index -= 1; | |
| const parent = ancestors[index]; | |
| buffer.text += print(index, `%c</${parent.name}>`); | |
| buffer.styles.push( | |
| COMPONENT_COLOR | |
| ); | |
| } | |
| console.log(buffer.text, ...buffer.styles); | |
| console.groupEnd(); | |
| } | |
| module.exports = findOffscreenViews; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| function getReactRoot() { | |
| // we can't cache a reference to this in a higher scope, because this isn't set initially | |
| // eslint-disable-next-line no-underscore-dangle | |
| const plugin = global.__REACT_DEVTOOLS_GLOBAL_HOOK__; | |
| const agent = plugin.reactDevtoolsAgent; | |
| const roots = Array.from(agent.roots); // roots is a set | |
| if (roots.length === 0) { | |
| return null; | |
| } | |
| const rootId = roots[roots.length - 1]; | |
| const root = agent.reactElements.get(rootId); | |
| return root; | |
| } | |
| module.exports = getReactRoot; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ... | |
| global.__findOffscreenViews = require('./findOffscreenViews'); | |
| ... |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment