6 |
7 | # reactopt
8 | [](https://badge.fury.io/js/reactopt)
9 |
10 | A CLI React performance optimization tool that identifies potential unnecessary re-rendering.
11 |
12 | # About
13 | Reactopt identifies specific events that may be causing unnecessary re-rendering of components in your application, and which components may benefit from utilizing shouldComponentUpdate.
14 |
15 | Prior to React 16, the module react-addons-perf helped identify locations that developers may want to implement shouldComponentUpdate to limit over-rendering. However, since the module is no longer supported we created Reactopt to fill the gap, and also provide increased functionality for any version of React.
16 |
17 | Upon initiating Reactopt, your application will be launched in a browser for you to interact with. After you're finished and type 'done', you will see an audit on your application's component performance.
18 |
19 | 1.5.1 is the first working verison of this module.
20 |
21 |
22 |
23 | ## Install and Use
24 | npm install
25 | ```bash
26 | npm install --save-dev reactopt
27 | ```
28 |
29 | Include this code at the top of your main React component file:
30 | ```js
31 | import { reactopt } from 'reactopt';
32 | reactopt(React);
33 | ```
34 |
35 | Include this script in your package.json:
36 | ```js
37 | "reactopt": "node node_modules/reactopt/main.js"
38 | ```
39 |
40 | Run command
41 | ```bash
42 | npm run reactopt localhost:####
43 | ```
44 |
45 | ## Team
46 | This module was created by [Candace Rogers](https://github.com/candacerogue), [Pam Lam](https://github.com/itspamlam), [Vu Phung](https://github.com/Jin6Coding), [Selina Zawacki](https://github.com/szmoon)
47 |
48 | ## Contact
49 | Like our app, found a bug?
50 |
51 | Let us know!
52 |
53 | [reactopt@gmail.com](reactopt@gmail.com)
54 |
55 | Visit us at www.reactopt.com
56 |
57 | ## Credit
58 | Utilizes a modified version of ([why-did-you-update by maicki](https://github.com/maicki/why-did-you-update))
59 |
--------------------------------------------------------------------------------
/src/deepDiff.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, '__esModule', {
4 | value: true
5 | });
6 |
7 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
8 |
9 | var _lodashIsEqual = require('lodash/isEqual');
10 |
11 | var _lodashIsEqual2 = _interopRequireDefault(_lodashIsEqual);
12 |
13 | var _lodashIsFunction = require('lodash/isFunction');
14 |
15 | var _lodashIsFunction2 = _interopRequireDefault(_lodashIsFunction);
16 |
17 | var _lodashKeys = require('lodash/keys');
18 |
19 | var _lodashKeys2 = _interopRequireDefault(_lodashKeys);
20 |
21 | var _lodashUnion = require('lodash/union');
22 |
23 | var _lodashUnion2 = _interopRequireDefault(_lodashUnion);
24 |
25 | var _lodashFilter = require('lodash/filter');
26 |
27 | var _lodashFilter2 = _interopRequireDefault(_lodashFilter);
28 |
29 | var _lodashEvery = require('lodash/every');
30 |
31 | var _lodashEvery2 = _interopRequireDefault(_lodashEvery);
32 |
33 | var _lodashPick = require('lodash/pick');
34 |
35 | var _lodashPick2 = _interopRequireDefault(_lodashPick);
36 |
37 | var DIFF_TYPES = {
38 | UNAVOIDABLE: 'unavoidable',
39 | SAME: 'same',
40 | EQUAL: 'equal',
41 | FUNCTIONS: 'functions'
42 | };
43 |
44 | exports.DIFF_TYPES = DIFF_TYPES;
45 | // called when componentDidUpdate is called (which means comp rerendered)
46 | // comparing previous state to next state, if they're the same, return console.log with this info
47 | var classifyDiff = function classifyDiff(prev, next, name) {
48 | // UNKNOWN for now, but guess is one is for state and one for props
49 | if (prev === next) {
50 | return {
51 | type: DIFF_TYPES.SAME,
52 | name: name,
53 | prev: prev,
54 | next: next
55 | };
56 | }
57 |
58 | //
59 | if ((0, _lodashIsEqual2['default'])(prev, next)) {
60 | return {
61 | type: DIFF_TYPES.EQUAL,
62 | name: name,
63 | prev: prev,
64 | next: next
65 | };
66 | }
67 |
68 | if (!prev || !next) {
69 | return {
70 | type: DIFF_TYPES.UNAVOIDABLE,
71 | name: name,
72 | prev: prev,
73 | next: next
74 | };
75 | }
76 |
77 | var isChanged = function isChanged(key) {
78 | return prev[key] !== next[key] && !(0, _lodashIsEqual2['default'])(prev[key], next[key]);
79 | };
80 | var isSameFunction = function isSameFunction(key) {
81 | var prevFn = prev[key];
82 | var nextFn = next[key];
83 | return (0, _lodashIsFunction2['default'])(prevFn) && (0, _lodashIsFunction2['default'])(nextFn) && prevFn.name === nextFn.name;
84 | };
85 |
86 | var keys = (0, _lodashUnion2['default'])((0, _lodashKeys2['default'])(prev), (0, _lodashKeys2['default'])(next));
87 | var changedKeys = (0, _lodashFilter2['default'])(keys, isChanged);
88 |
89 | if (changedKeys.length && (0, _lodashEvery2['default'])(changedKeys, isSameFunction)) {
90 | return {
91 | type: DIFF_TYPES.FUNCTIONS,
92 | name: name,
93 | prev: (0, _lodashPick2['default'])(prev, changedKeys),
94 | next: (0, _lodashPick2['default'])(next, changedKeys)
95 | };
96 | }
97 | return {
98 | type: DIFF_TYPES.UNAVOIDABLE,
99 | name: name,
100 | prev: prev,
101 | next: next
102 | };
103 | };
104 | exports.classifyDiff = classifyDiff;
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | //load image
4 | require('console-png').attachTo(console);
5 | let image = require('fs').readFileSync(__dirname + '/media/logo-cli.png');
6 |
7 | // chalk requirements
8 | const chalk = require('chalk');
9 | const log = console.log;
10 |
11 | const puppeteer = require('puppeteer');
12 | // middleware for reading cli text input
13 | const readline = require('readline');
14 | const rl = readline.createInterface({
15 | input: process.stdin,
16 | output: process.stdout
17 | });
18 |
19 | // placeholder for data object window events
20 | let data; // comment out for testing
21 |
22 | // // data for testing suite
23 | // let data = {
24 | // time: '0ms',
25 | // rerenders : [{
26 | // type: 'initialLoad',
27 | // name: 'initialLoad',
28 | // components: []
29 | // }]
30 | // };
31 |
32 | let uri = process.argv[2]; // gets url from CLI "npm start [url]"
33 |
34 | //start puppeteer to allow user to interact with React app
35 | puppeteer.launch({headless: false}).then(async browser => {
36 | const page = await browser.newPage();
37 | await page.setViewport({width: 1000, height: 1000}); // chromium default is 800 x 600 px
38 | await page.goto(uri);
39 |
40 | //close browser on 'done' but also grab data before closing browser
41 | await rl.on('line', (line) => {
42 | if (line === 'done') {
43 | page.evaluate(() => {
44 | return window.data
45 | }).then((returnedData) => {
46 | data = returnedData;
47 | browser.close();
48 | return data;
49 | }).then(logAudits)
50 | .catch((err) => console.log('Error, no data collected. Try interacting more with your page.'));
51 | }
52 | });
53 | });
54 |
55 | //runs on start of reactopt
56 | (function startReactopt() {
57 | console.png(image);
58 | setTimeout(reactoptRun,1000);
59 | function reactoptRun() {
60 | log('');
61 | log('');
62 | log(chalk.bgGreen.bold(" Reactopt is running - Interact with your app, don't close the browser, then type 'done' to perform audit. "));
63 | log('');
64 | }
65 | })(); // iife
66 |
67 | // when user ends interaction with 'done', execute these audits
68 | function logAudits() {
69 | var funcArray = [
70 | loadTime,
71 | componentRerenders
72 | ];
73 |
74 | // run functions in funcArray, with printLine prior to each
75 | funcArray.forEach((eventsMethod) => {
76 | printLine();
77 | eventsMethod(data);
78 | });
79 | }
80 |
81 | // styling for different console logs
82 | function printLine(type, string) {
83 | switch (type) {
84 | case 'heading':
85 | log(chalk.black.bgWhite.dim(string));
86 | log('');
87 | break;
88 | case 'pass':
89 | log(chalk.green.bold(string));
90 | break;
91 | case 'fail':
92 | log(chalk.red.bold(string));
93 | break;
94 | case 'suggestion':
95 | log(chalk.gray(string));
96 | break;
97 | case 'line':
98 | log('');
99 | log(chalk.gray('-----------------------------------------------------------------------------------'));
100 | log('');
101 | break;
102 | default:
103 | break;
104 | }
105 | }
106 |
107 | let indentD = ' $ ';
108 | let indent = ' ';
109 |
110 | // test functions
111 | function loadTime(data) {
112 | printLine('heading', 'Page Load Time');
113 | log(indent + 'Your page took ' + data.time + ' to load');
114 | log('');
115 | }
116 |
117 | function componentRerenders(data) {
118 | printLine('heading', 'Component Re-rendering');
119 |
120 | if (data.rerenders.length > 1) {
121 | printLine('fail', 'There are components that are potentially re-rendering unnecessarily. Below are identified events that triggered them:');
122 | log('');
123 | // print eventTypes, eventNames, and components rerendered for each unnecessary rerendering
124 | for (let i = 1; i < data.rerenders.length; i += 1) {
125 | if ((data.rerenders[i].components).length > 0) {
126 | log(indentD + chalk.underline(data.rerenders[i].type + ' - ' + data.rerenders[i].name) + ' => ' + data.rerenders[i].components);
127 | }
128 | }
129 | log('');
130 | // print suggestions for possible improvements
131 | log(chalk.italic('Possible improvements to re-rendering'));
132 | log('');
133 | printLine('suggestion', indent + "* " + "Consider utilizing shouldComponentDidUpdate of components that shouldn't be constantly re-rendering");
134 | printLine('suggestion', indent + "* " + "Note: this may affect functionality of child components");
135 | } else {
136 | printLine('pass', indent + 'Way to go, Idaho! No unnecessary re-rendering of components were detected.');
137 | log('');
138 | }
139 | }
140 |
141 | Object.defineProperty(exports, '__esModule', {
142 | value: true
143 | });
144 |
145 | // exports.data = data;
146 | module.exports = { data, loadTime, printLine, componentRerenders };
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // declare exports
4 | Object.defineProperty(exports, '__esModule', {
5 | value: true
6 | });
7 |
8 | var _deepDiff = require('./deepDiff');
9 | var _getDisplayName = require('./getDisplayName');
10 | var _normalizeOptions = require('./normalizeOptions');
11 | var _shouldInclude = require('./shouldInclude');
12 |
13 | window.data = {
14 | time: '',
15 | rerenders : [{
16 | type: 'initialLoad',
17 | name: 'initialLoad',
18 | components: []
19 | }]
20 | };
21 |
22 | var keyboardEvents = ['keypress', 'keydown', 'input'];
23 | var mouseEvents = ['click', 'dbclick', 'drag'];
24 |
25 | // monkeypatch
26 | // ****** called on render -> look down to opts.notifier
27 | function createComponentDidUpdate(opts) {
28 | return function componentDidUpdate(prevProps, prevState) {
29 | //displayname is component name
30 | var displayName = (0, _getDisplayName.getDisplayName)(this);
31 |
32 | //should include returns display/comp name, if return value doesn't exist exit compDidUpdate w/o doing anything
33 | if (!(0, _shouldInclude.shouldInclude)(displayName, opts)) {
34 | return;
35 | }
36 |
37 | var propsDiff = (0, _deepDiff.classifyDiff)(prevProps, this.props, displayName + '.props');
38 | if (propsDiff.type === _deepDiff.DIFF_TYPES.UNAVOIDABLE) {
39 | return;
40 | }
41 |
42 | var stateDiff = (0, _deepDiff.classifyDiff)(prevState, this.state, displayName + '.state');
43 | if (stateDiff.type === _deepDiff.DIFF_TYPES.UNAVOIDABLE) {
44 | return;
45 | }
46 | //if makes it past above non-conflicts (meaning there are components re-rendering unnecessarily)
47 | // temp timeout because event listener sets global event names after components re-render
48 | setTimeout(timeTest,100);
49 | function timeTest() {
50 | console.log('data!!!',window.data);
51 |
52 | // storing event data in window's 'data' object
53 | let len = (window.data.rerenders).length - 1;
54 | window.data.rerenders[len].components.push(displayName);
55 | }
56 | };
57 | }
58 |
59 | // takes in react component, triggers all other logic, is exported out
60 | var whyDidYouUpdate = function whyDidYouUpdate(React) {
61 |
62 | // event listener for load page
63 | window.addEventListener('load', () => {
64 | // calculation for total time taken to render the webpage
65 | const startLoadTime = window.performance.timing.loadEventStart
66 | const endLoadTime = window.performance.timing.domLoading
67 | const deltaTime = startLoadTime - endLoadTime;
68 |
69 | window.data.time = deltaTime + 'ms';
70 | });
71 |
72 | /**
73 | * function handler for click, dbclick, and drag
74 | */
75 | function handleMouseEvents(e) {
76 | let localName = e.target.localName;
77 | let innerText = '';
78 |
79 | // description string for click elements
80 | function setInnerText(type, targetInfo) {
81 | innerText = type +': ' + targetInfo;
82 | }
83 | // if clicked element is '