From 57324467f47e311628a37b0712fef8c66cb111aa Mon Sep 17 00:00:00 2001 From: Vincent Bailly Date: Fri, 1 Feb 2019 13:52:21 +0100 Subject: [PATCH] first commit --- .gitignore | 61 +------------------------- package.json | 28 ++++++++++++ src/index.tsx | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 20 +++++++++ yarn.lock | 69 +++++++++++++++++++++++++++++ 5 files changed, 235 insertions(+), 60 deletions(-) create mode 100644 package.json create mode 100644 src/index.tsx create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore index ad46b30..c18ed01 100644 --- a/.gitignore +++ b/.gitignore @@ -1,61 +1,2 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - -# next.js build output -.next +lib/ \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..cb9f092 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "wastedrendersdetector", + "version": "1.0.0", + "description": "Tool to debug wasted renders", + "main": "lib/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "tsc", + "prepare": "yarn build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Microsoft/WastedRendersDetector.git" + }, + "author": "", + "license": "MIT", + "bugs": { + "url": "https://github.com/Microsoft/WastedRendersDetector/issues" + }, + "homepage": "https://github.com/Microsoft/WastedRendersDetector#readme", + "dependencies": { + "react": ">=16.0.0" + }, + "devDependencies": { + "@types/react": ">=16.0.0", + "typescript": "^3.3.1" + } +} diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..b95717a --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,117 @@ +import * as React from "react"; + +class WarningComponent extends React.Component { + public render(): null { + return null; + } + + public componentDidUpdate(prevProps: any): void { + if (!prevProps || !this.props) { + return; + } + + // If some props where added or removed then the update is legit. + if (propsAddedOrRemoved(prevProps, this.props)) { + return; + } + + const differences = deepSubstract(prevProps, this.props); + + if (differences === null) { + console.error("🚨 WASTED RENDER 🚨, props changing reference for nothing: ", shallowSubstract(prevProps, this.props)); + } else if (isOnlyFunctions(differences)) { + console.error("🚨 WASTED RENDER 🚨, props changed because of function reference(s) changing: ", differences); + } + } +} + +function propsAddedOrRemoved(props1: any, props2: any): boolean { + const keysProps1 = Object.keys(props1); + const keysProps2 = Object.keys(props2); + return keysProps1.length !== keysProps2.length || keysProps1.some(k => keysProps2.indexOf(k) === -1); +} + +/* + * Takes a difference object and check if it contains data or only functions. + */ +function isOnlyFunctions(a: any): boolean { + if (typeof a === "function") { + return true; + } + + if (typeof a !== "object") { + return false; + } + + return Object.keys(a).every(k => isOnlyFunctions(a[k])); +} + +/* + * Returns "shallow" A - B + * Supported types: object + */ +function shallowSubstract(a: any, b: any): object | null { + const n: any = {}; + Object.keys(a).forEach(key => { + if (a[key] !== b[key]) { + n[key] = a[key]; + } + }); + + if (Object.keys(n).length) { + return n; + } + + return null; +} + +/* + * Returns "deep" A - B + * Supported types: any + * Not supported: circular references + */ +function deepSubstract(a: any, b: any): object | null { + if (a === b) { + return null; + } + + if (typeof a !== "object" || typeof b !== "object") { + return a; + } + + const n: any = {}; + + Object.keys(a).forEach(key => { + if (Object.keys(b).indexOf(key) === -1) { + n[key] = a[key]; + } else { + const diff = deepSubstract(a[key], b[key]); + if (diff !== null) { + n[key] = diff; + } + } + }); + + if (Object.keys(n).length) { + return n; + } + + return null; +} + +/* + * HOC that checks that on every update whether the props update actually represents a real update. + * We do this by checking that props update represent a real update of data. + * We also check that the update is not solely due to a function changing reference. + * As a result, errors will be logged in the console in case of wasted renders. + * This component should only be used locally for troublshooting, + * no usage of it should be checked-in because using it is expensive. + */ +export function debugOnlyWastedRenderDetector(Component: React.ComponentType): React.ComponentType { + return (props: T) => ( + + + + + ); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..64c5797 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2015", + "lib": ["es2015", "dom"], + "rootDir": "src", + "outDir": "lib", + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "declarationMap": true, + "inlineSourceMap": true, + "inlineSources": false, + "jsx": "react", + "declaration": true, + "module": "esnext", + "moduleResolution": "node", + "preserveWatchOutput": true + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..d4c3f6f --- /dev/null +++ b/yarn.lock @@ -0,0 +1,69 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/prop-types@*": + version "15.5.8" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.8.tgz#8ae4e0ea205fe95c3901a5a1df7f66495e3a56ce" + integrity sha512-3AQoUxQcQtLHsK25wtTWIoIpgYjH3vSDroZOUr7PpCHw/jLY1RB9z9E8dBT/OSmwStVgkRNvdh+ZHNiomRieaw== + +"@types/react@>=16.0.0": + version "16.7.22" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.7.22.tgz#5bc6d166d5ac34b835756f0b736c7b1af0043e81" + integrity sha512-j/3tVoY09kHcTfbia4l67ofQn9xvktUvlC/4QN0KuBHAXlbU/wuGKMb8WfEb/vIcWxsOxHv559uYprkFDFfP8Q== + dependencies: + "@types/prop-types" "*" + csstype "^2.2.0" + +csstype@^2.2.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.2.tgz#3043d5e065454579afc7478a18de41909c8a2f01" + integrity sha512-Rl7PvTae0pflc1YtxtKbiSqq20Ts6vpIYOD5WBafl4y123DyHUeLrRdQP66sQW8/6gmX8jrYJLXwNeMqYVJcow== + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +loose-envify@^1.1.0, loose-envify@^1.3.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +prop-types@^15.6.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" + integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ== + dependencies: + loose-envify "^1.3.1" + object-assign "^4.1.1" + +react@>=16.0.0: + version "16.7.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.7.0.tgz#b674ec396b0a5715873b350446f7ea0802ab6381" + integrity sha512-StCz3QY8lxTb5cl2HJxjwLFOXPIFQp+p+hxQfc8WE0QiLfCtIlKj8/+5tjjKm8uSTlAW+fCPaavGFS06V9Ar3A== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.12.0" + +scheduler@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.12.0.tgz#8ab17699939c0aedc5a196a657743c496538647b" + integrity sha512-t7MBR28Akcp4Jm+QoR63XgAi9YgCUmgvDHqf5otgAj4QvdoBE4ImCX0ffehefePPG+aitiYHp0g/mW6s4Tp+dw== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +typescript@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.1.tgz#6de14e1db4b8a006ac535e482c8ba018c55f750b" + integrity sha512-cTmIDFW7O0IHbn1DPYjkiebHxwtCMU+eTy30ZtJNBPF9j2O1ITu5XH2YnBeVRKWHqF+3JQwWJv0Q0aUgX8W7IA==