1<!DOCTYPE html><meta charSet="utf-8"/><meta http-equiv="x-ua-compatible" content="ie=edge"/><meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/><meta name="generator" content="Gatsby 5.16.1"/><meta name="description" content="How to speed up React interactions 4à with Chrome DevTools, React Profiler, and just a few line changes" data-gatsby-head="true"/><meta name="image" content="https://3perf.com/static/5670f5af54e2a958f519180ec2014131/causal-meta.png" data-gatsby-head="true"/><meta name="twitter:card" content="summary_large_image" data-gatsby-head="true"/><meta name="twitter:title" content="Case Study: Making React Interactions in Causal 4à Faster" data-gatsby-head="true"/><meta name="twitter:description" content="How to speed up React interactions 4à with Chrome DevTools, React Profiler, and just a few line changes" data-gatsby-head="true"/><meta name="twitter:site" content="@3perfcom" data-gatsby-head="true"/><meta name="twitter:creator" content="@iamakulov" data-gatsby-head="true"/><meta name="twitter:image:src" content="https://3perf.com/static/5670f5af54e2a958f519180ec2014131/causal-meta.png" data-gatsby-head="true"/><meta name="og:title" content="Case Study: Making React Interactions in Causal 4à Faster" data-gatsby-head="true"/><meta name="og:description" content="How to speed up React interactions 4à with Chrome DevTools, React Profiler, and just a few line changes" data-gatsby-head="true"/><meta name="og:image" content="https://3perf.com/static/5670f5af54e2a958f519180ec2014131/causal-meta.png" data-gatsby-head="true"/><meta name="og:site_name" content="PerfPerfPerf" data-gatsby-head="true"/><meta name="fb:admins" content="100002052594007" data-gatsby-head="true"/><meta name="og:type" content="article" data-gatsby-head="true"/><meta name="article:published_time" content="2022-11-24T09:00:00.000Z" data-gatsby-head="true"/><meta name="article:author" content="Ivan Akulov" data-gatsby-head="true"/><meta name="article:modified_time" content="2022-11-24T09:00:00.000Z" data-gatsby-head="true"/><link rel="sitemap" type="application/xml" href="/sitemap-index.xml"/><link rel="icon" href="/favicon-32x32.png?v=896ef987c9f87d07396089ed2e630f97" type="image/png"/><link rel="manifest" href="/manifest.webmanifest" crossorigin="anonymous"/><link rel="apple-touch-icon" sizes="48x48" href="/icons/icon-48x48.png?v=896ef987c9f87d07396089ed2e630f97"/><link rel="apple-touch-icon" sizes="72x72" href="/icons/icon-72x72.png?v=896ef987c9f87d07396089ed2e630f97"/><link rel="apple-touch-icon" sizes="96x96" href="/icons/icon-96x96.png?v=896ef987c9f87d07396089ed2e630f97"/><link rel="apple-touch-icon" sizes="144x144" href="/icons/icon-144x144.png?v=896ef987c9f87d07396089ed2e630f97"/><link rel="apple-touch-icon" sizes="192x192" href="/icons/icon-192x192.png?v=896ef987c9f87d07396089ed2e630f97"/><link rel="apple-touch-icon" sizes="256x256" href="/icons/icon-256x256.png?v=896ef987c9f87d07396089ed2e630f97"/><link rel="apple-touch-icon" sizes="384x384" href="/icons/icon-384x384.png?v=896ef987c9f87d07396089ed2e630f97"/><link rel="apple-touch-icon" sizes="512x512" href="/icons/icon-512x512.png?v=896ef987c9f87d07396089ed2e630f97"/><link rel="alternate" type="application/rss+xml" title="PerfPerfPerf Blog" href="/rss.xml"/><title data-gatsby-head="true">Case Study: Making React Interactions in Causal 4à Faster · PerfPerfPerf</title><link rel="canonical" href="https://3perf.com/blog/causal/" data-gatsby-head="true"/><html lang="en"><head><meta itemProp="name" content="Case Study: Making React Interactions in Causal 4à Faster" data-gatsby-head="true"/><meta itemProp="description" content="How to speed up React interactions 4à with Chrome DevTools, React Profiler, and just a few line changes" data-gatsby-head="true"/><meta itemProp="image" content="https://3perf.com/static/5670f5af54e2a958f519180ec2014131/causal-meta.png" data-gatsby-head="true"/><style data-href="/styles.b5c5436dd0507acd97e4.css" data-identity="gatsby-global-css">@font-face{font-display:swap;font-family:Bricolage Grotesque Variable;font-stretch:75% 100%;font-style:normal;font-weight:200 800;src:url(/static/bricolage-grotesque-vietnamese-standard-normal-315cef462476639efb58bdf85db6adf9.woff2) format("woff2-variations");
1unicode-range:u+0102-0103,u+0110-0111,u+0128-0129,u+0168-0169,u+01a0-01a1,u+01af-01b0,u+0300-0301,u+0303-0304,u+0308-0309,u+0323,u+0329,u+1ea0-1ef9,u+20ab}@font-face{font-display:swap;font-family:Bricolage Grotesque Variable;font-stretch:75% 100%;font-style:normal;font-weight:200 800;src:url(/static/bricolage-grotesque-latin-ext-standard-normal-a84345800ca1888afc1e9ebb595cc2d5.woff2) format("woff2-variations");unicode-range:u+0100-02ba,u+02bd-02c5,u+02c7-02cc,u+02ce-02d7,u+02dd-02ff,u+0304,u+0308,u+0329,u+1d00-1dbf,u+1e00-1e9f,u+1ef2-1eff,u+2020,u+20a0-20ab,u+20ad-20c0,u+2113,u+2c60-2c7f,u+a720-a7ff}@font-face{font-display:swap;font-family:Bricolage Grotesque Variable;font-stretch:75% 100%;font-style:normal;font-weight:200 800;src:url(/static/bricolage-grotesque-latin-standard-normal-e956cf557f2fb3fb7bed172a0ae8b6cd.woff2) format("woff2-variations");unicode-range:u+00??,u+0131,u+0152-0153,u+02bb-02bc,u+02c6,u+02da,u+02dc,u+0304,u+0308,u+0329,u+2000-206f,u+20ac,u+2122,u+2191,u+2193,u+2212,u+2215,u+feff,u+fffd}@font-face{font-display:swap;
1font-family:Fira Code;font-style:normal;font-weight:600;src:url(/static/fira-code-latin-600-normal-7ee6420189e6ca18a7d4f07c24ebbda7.woff2) format("woff2"),url(/static/fira-code-latin-600-normal-40873d2585f5b229e8e229799faed247.woff) format("woff")}@font-face{font-display:swap;font-family:Fira Code;font-style:normal;font-weight:400;src:url(/static/fira-code-latin-400-normal-e662d57ce51fba1f46e32dff4dd23e4d.woff2) format("woff2"),url(/static/fira-code-latin-400-normal-88fb5395c38bbc52be72b1063bc16fff.woff) format("woff")}</style><style data-styled="" data-styled-version="6.0.4">*,*::before,*::after{box-sizing:border-box;}/*!sc*/ 2html{font-family:'Bricolage Grotesque Variable',sans-serif;overflow-x:hidden;--link-color:#06c;--link-border-color:rgba(0,102,204,0.25);--link-active-color:#d04000;--link-active-border-color:rgba(208,64,0,0.25);}/*!sc*/ 3body{margin:0;font-size:16px;line-height:1.4;overflow:hidden;}/*!sc*/ 4a{color:var(--link-color);text-decoration:none;cursor:pointer;border-bottom:1px solid;border-color:var(--link-border-color);transition:color 0.25s ease-out,border-color 0.25s ease-out;}/*!sc*/ 5a:hover,a:focus,a:active{color:var(--link-active-color);border-color:var(--link-active-border-color);transition:none;}/*!sc*/ 6code{font-family:'Fira Code',Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;background:rgba(0, 0, 0, 0.06);padding:0 3px;border-radius:2px;}/*!sc*/ 7p{margin:0;}/*!sc*/ 8p+p{margin-top:16px;}/*!sc*/ 9li{margin:0;}/*!sc*/ 10li+li{margin-top:16px;}/*!sc*/ 11data-styled.g1[id="sc-global-ftzMja1"]{content:"sc-global-ftzMja1,"}/*!sc*/ 12.jLbNpk #perf-letters path{fill:black;}/*!sc*/ 13.jLbNpk rect{stroke:black;}/*!sc*/ 14data-styled.g2[id="styled__LogoSvg-sc-lzfaox-0"]{content:"jLbNpk,"}/*!sc*/ 15.fHUXzs{margin:0 auto;max-width:1000px;width:100%;padding:0 20px;}/*!sc*/ 16@media print{.fHUXzs{max-width:none;}}/*!sc*/ 17data-styled.g3[id="WidthWrapper-sc-1hg7f1n-0"]{content:"fHUXzs,"}/*!sc*/ 18.ewhQKw{display:inline-block;overflow:hidden;line-height:0;}/*!sc*/ 19data-styled.g4[id="styled__Container-sc-1ekqqun-0"]{content:"ewhQKw,"}/*!sc*/ 20.jViMaS{display:flex;flex-wrap:wrap;padding:0;margin:-8px -12px;list-style-type:none;}/*!sc*/ 21data-styled.g6[id="styled__Container-sc-exfs5j-0"]{content:"jViMaS,"}/*!sc*/ 22.bHlTQw{margin:8px 12px;}/*!sc*/ 23data-styled.g7[id="styled__Item-sc-exfs5j-1"]{content:"bHlTQw,"}/*!sc*/ 24.chErgn{display:block;border:none;width:min-content;}/*!sc*/ 25data-styled.g8[id="styled__ItemLink-sc-exfs5j-2"]{content:"chErgn,"}/*!sc*/ 26.kZXEoD{color:var(--link-color);text-decoration:none;cursor:pointer;border-bottom:1px solid;border-color:var(--link-border-color);transition:color 0.25s ease-out,border-color 0.25s ease-out;}/*!sc*/ 27.styled__ItemLink-sc-exfs5j-2:hover .kZXEoD,.styled__ItemLink-sc-exfs5j-2:focus .kZXEoD,.styled__ItemLink-sc-exfs5j-2:active .kZXEoD{color:var(--link-active-color);border-color:var(--link-active-border-color);transition:none;}/*!sc*/ 28data-styled.g10[id="styled__Label-sc-exfs5j-4"]{content:"kZXEoD,"}/*!sc*/ 29.qJRRG{display:flex;justify-content:space-between;}/*!sc*/ 30data-styled.g12[id="styled__Credentials-sc-fmk9on-1"]{content:"qJRRG,"}/*!sc*/ 31.eMPHzj{max-width:550px;margin-top:8px;font-size:10px;text-transform:uppercase;letter-spacing:1px;}/*!sc*/ 32data-styled.g13[id="styled__Legal-sc-fmk9on-2"]{content:"eMPHzj,"}/*!sc*/ 33.ecDLDl{letter-spacing:1px;}/*!sc*/ 34data-styled.g14[id="styled__LicenseName-sc-fmk9on-3"]{content:"ecDLDl,"}/*!sc*/ 35.YNXaB{margin-top:16px;}/*!sc*/ 36data-styled.g17[id="styled__MailchimpForm-sc-1pidu4x-1"]{content:"YNXaB,"}/*!sc*/ 37.ealAEm{margin-right:16px;margin-bottom:8px;height:32px;border:none;border-radius:2px;padding:0 12px;font-size:inherit;font-family:inherit;}/*!sc*/ 38data-styled.g18[id="styled__MailchimpInput-sc-1pidu4x-2"]{content:"ealAEm,"}/*!sc*/ 39.celNKA{cursor:pointer;height:32px;padding:0 12px;border-radius:2px;font-size:inherit;font-family:inherit;background:#ffdb01;color:black;border:none;}/*!sc*/ 40.celNKA:hover,.celNKA:focus,.celNKA:active{background:#ffe967;}/*!sc*/ 41data-styled.g19[id="styled__MailchimpSubmit-sc-1pidu4x-3"]{content:"celNKA,"}/*!sc*/ 42.hSVmgK{position:absolute;
42left:-5000px;}/*!sc*/ 43data-styled.g20[id="styled__HiddenDiv-sc-1pidu4x-4"]{content:"hSVmgK,"}/*!sc*/ 44.jLAQgJ{display:flex;align-items:center;flex-wrap:wrap;gap:12px 24px;}/*!sc*/ 45data-styled.g21[id="styled__Container-sc-1wdhdtf-0"]{content:"jLAQgJ,"}/*!sc*/ 46.hMAVWe{display:block;height:50px;width:50px;}/*!sc*/ 47data-styled.g22[id="styled__Logo-sc-1wdhdtf-1"]{content:"hMAVWe,"}/*!sc*/ 48.cmWeoQ{flex:1 0 auto;max-width:calc(100% - 24px);}/*!sc*/ 49data-styled.g23[id="styled__ItemsWrapper-sc-1wdhdtf-2"]{content:"cmWeoQ,"}/*!sc*/ 50.IrpCG{display:flex;gap:3px 12px;flex-wrap:wrap;}/*!sc*/ 51data-styled.g24[id="styled__Items-sc-1wdhdtf-3"]{content:"IrpCG,"}/*!sc*/ 52.hruSyn{display:block;border:none;}/*!sc*/ 53data-styled.g25[id="styled__BlockLink-sc-1wdhdtf-4"]{content:"hruSyn,"}/*!sc*/ 54@media (min-width:769px){.PaASP{margin-left:auto;}}/*!sc*/ 55data-styled.g26[id="styled__SecondaryItems-sc-1wdhdtf-5"]{content:"PaASP,"}/*!sc*/ 56.jkrHkn{margin-top:24px;margin-bottom:56px;font-size:16px;}/*!sc*/ 57data-styled.g27[id="styled__Nav-sc-g87c0z-0"]{content:"jkrHkn,"}/*!sc*/ 58.hjUmKx{margin-bottom:40px;}/*!sc*/ 59data-styled.g28[id="styled__Header-sc-g87c0z-1"]{content:"hjUmKx,"}/*!sc*/ 60.fpALeQ{margin-top:16px;}/*!sc*/ 61data-styled.g29[id="styled__TopMeta-sc-g87c0z-2"]{content:"fpALeQ,"}/*!sc*/ 62.bWLCcb{margin-top:32px;}/*!sc*/ 63data-styled.g30[id="styled__BottomMeta-sc-g87c0z-3"]{content:"bWLCcb,"}/*!sc*/ 64.gCDoRG{margin:0;font-size:60px;font-weight:900;line-height:1;}/*!sc*/ 65@media (max-width:768px){.gCDoRG{font-size:40px;}}/*!sc*/ 66data-styled.g31[id="styled__Title-sc-g87c0z-4"]{content:"gCDoRG,"}/*!sc*/ 67.loPRVe{margin:48px -24px;max-width:600px;padding:16px 24px;border-radius:4px;background:#fffae0;}/*!sc*/ 68data-styled.g32[id="styled__MailchimpSubscribe-sc-g87c0z-5"]{content:"loPRVe,"}/*!sc*/ 69.jHbsYy{margin-top:48px;}/*!sc*/ 70data-styled.g33[id="styled__BlogFooterAccordion-sc-g87c0z-6"]{content:"jHbsYy,"}/*!sc*/ 71.kMmgzr{margin-top:48px;margin-bottom:16px;}/*!sc*/ 72data-styled.g34[id="styled__Footer-sc-g87c0z-7"]{content:"kMmgzr,"}/*!sc*/ 73.dhCohm{font-family:system-ui,sans-serif;max-width:600px;}/*!sc*/ 74.dhCohm h1,.dhCohm h2,.dhCohm h3,.dhCohm h4,.dhCohm h5,.dhCohm h6{font-family:'Bricolage Grotesque Variable',sans-serif;margin-top:2em;}/*!sc*/ 75.dhCohm h1 .anchor,.dhCohm h2 .anchor,.dhCohm h3 .anchor,.dhCohm h4 .anchor,.dhCohm h5 .anchor,.dhCohm h6 .anchor{border:none;}/*!sc*/ 76.dhCohm h1{font-size:32px;font-weight:900;}/*!sc*/ 77.dhCohm h2{font-size:24px;}/*!sc*/ 78.dhCohm h3{font-size:18px;}/*!sc*/ 79.dhCohm .media-container{margin:0;}/*!sc*/ 80.dhCohm .media-container img,.dhCohm .media-container video{width:min(max(100%,80vw),900px);height:auto;}/*!sc*/ 81.dhCohm .media-container img{border-radius:2px;}/*!sc*/ 82.dhCohm .media-container a{display:block;border-bottom:none;}/*!sc*/ 83.dhCohm .media-container_border img,.dhCohm .media-container_border video{outline:1px solid rgba(0,0,0,0.05);outline-offset:-1px;}/*!sc*/ 84.dhCohm .sidenote__heading .media-container img,.dhCohm .note .media-container img{width:100%;}/*!sc*/ 85.dhCohm .media-container__caption{font-size:13px;font-style:italic;margin-top:4px;}/*!sc*/ 86.dhCohm .media-container__caption em{font-style:normal;}/*!sc*/ 87.dhCohm .media-container_scrollable{width:100vw;overflow-x:auto;margin-left:-20px;margin-right:-20px;padding-left:20px;padding-right:20px;}/*!sc*/ 88.dhCohm .media-container_scrollable::-webkit-scrollbar{height:4px;background:#eee;}/*!sc*/ 89.dhCohm .media-container_scrollable::-webkit-scrollbar-thumb{background:#ccc;border-radius:4px;}/*!sc*/ 90.dhCohm ol,.dhCohm ul{margin:0;padding-left:32px;}/*!sc*/ 91.dhCohm p+.list_compact.list_compact,.dhCohm .list_compact.list_compact+p{margin-top:8px;}/*!sc*/ 92.dhCohm .list_compact li+li{margin-top:0;}/*!sc*/ 93.dhCohm .list_spread li+li{margin-top:16px;}/*!sc*/ 94.dhCohm .gatsby-highlight{overflow-x:auto;margin:0 -16px;padding:8px 16px;background:rgba(0, 0, 0, 0.06);border-radius:2px;}/*!sc*/ 95.dhCohm .gatsby-highlight >pre{margin:0;}/*!sc*/ 96.dhCohm .gatsby-highlight >pre>code{padding:0;background:unset;}/*!sc*/ 97.dhCohm .gatsby-highlight[data-word-wrap] >pre,.dhCohm .gatsby-highlight[data-word-wrap] >pre>code{white-space:pre-wrap;overflow-wrap:break-word;}/*!sc*/ 98.dhCohm code[class*='language-'],.dhCohm pre[class*='language-']{text-align:left;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:2;-o-tab-size:2;tab-size:2;hyphens:none;}/*!sc*/ 99.dhCohm pre[class*='language-']{white-space:pre;}/*!sc*/ 100.dhCohm pre[class*='language-']::selection,.dhCohm pre[class*='language-'] ::selection,.dhCohm code[class*='language-']::selection,.dhCohm code[class*='language-'] ::selection{text-shadow:none;background:#b3d4fc;}/*!sc*/ 101.dhCohm .token.comment,.dhCohm .token.prolog,.dhCohm .token.doctype,.dhCohm .token.cdata{color:slategray;}/*!sc*/ 102.dhCohm .token.punctuation{color:#999;}/*!sc*/ 103.dhCohm .namespace{opacity:0.7;}/*!sc*/ 104.dhCohm .token.property,.dhCohm .token.tag,.dhCohm .token.boolean,.dhCohm .token.number,.dhCohm .token.constant,.dhCohm .token.symbol,.dhCohm .token.deleted{color:#905;}/*!sc*/ 105.dhCohm .token.selector,.dhCohm .token.attr-name,.dhCohm .token.string,.dhCohm .token.char,.dhCohm .token.builtin,.dhCohm .token.inserted{color:#690;}/*!sc*/ 106.dhCohm .token.operator,.dhCohm .token.entity,.dhCohm .token.url,.dhCohm .language-css .token.string,.dhCohm .style .token.string{color:#9a6e3a;}/*!sc*/ 107.dhCohm .token.atrule,.dhCohm .token.attr-value,.dhCohm .token.keyword{color:#07a;}/*!sc*/ 108.dhCohm .token.function,.dhCohm .token.class-name{color:#dd4a68;}/*!sc*/ 109.dhCohm .token.regex,.dhCohm .token.important,.dhCohm .token.variable{color:#e90;}/*!sc*/ 110.dhCohm .token.important,.dhCohm .token.bold{font-weight:bold;}
110/*!sc*/ 111.dhCohm .token.italic{font-style:italic;}/*!sc*/ 112.dhCohm .token.entity{cursor:help;}/*!sc*/ 113@media (min-width:1025px){.dhCohm .sidenote{display:flex;align-items:baseline;width:calc(100% + 300px + 48px);}.dhCohm .sidenote__body{width:calc(100% - 300px - 48px);}.dhCohm .sidenote__heading{margin-left:48px;width:300px;font-size:13px;height:0;}.dhCohm .sidenote__remark{display:none;}}/*!sc*/ 114@media (max-width:1024px){.dhCohm .sidenote__heading{margin:8px 0 16px;font-size:13px;}}/*!sc*/ 115.dhCohm .toc{background:#fffae0;width:fit-content;margin:16px -16px;padding:12px 40px 16px 16px;font-size:13px;border-radius:2px;font-family:'Bricolage Grotesque Variable',sans-serif;}/*!sc*/ 116.dhCohm .toc.toc_with-header{margin:32px -16px;padding:16px 40px 24px 16px;}/*!sc*/ 117.dhCohm .toc ul{padding-left:24px;list-style:none;}/*!sc*/ 118.dhCohm .toc li{margin-top:8px;}/*!sc*/ 119.dhCohm .toc >ul{padding-left:0;}/*!sc*/ 120.dhCohm .toc >ul>li:first-child{margin-top:0;}/*!sc*/ 121.dhCohm .toc ul:has(> li:only-child > ul:only-child){padding-left:0;}/*!sc*/ 122.dhCohm .toc *:is(p,ul)+*:is(p,ul){margin-top:0!important;}/*!sc*/ 123.dhCohm .toc__header{margin:0 0 8px;font-size:26px;}/*!sc*/ 124.dhCohm .note{background:#fffae0;margin:16px -16px;padding:8px 16px;font-size:13px;border-radius:2px;}/*!sc*/ 125.dhCohm .note >.gatsby-highlight:last-child{margin-bottom:-8px;}/*!sc*/ 126.dhCohm *:is(p,.note,.sidenote,.gatsby-highlight,ul,ol,.media-container)+*:is(p,.note,.sidenote,.gatsby-highlight,ul,ol,.media-container){margin-top:16px;}/*!sc*/ 127.dhCohm blockquote:not(.sidenote__heading):not(.note){position:relative;margin:16px 0;border-left:#333 5px solid;padding-left:24px;}/*!sc*/ 128.dhCohm blockquote:not(.sidenote__heading):not(.note)::before{content:'â';position:absolute;left:-16px;top:0;display:block;height:30px;overflow:hidden;line-height:40px;font-size:60px;font-weight:900;color:#333;background:white;}/*!sc*/ 129.dhCohm mark{background:#fffae0;}/*!sc*/ 130.dhCohm hr{margin-top:32px;margin-bottom:32px;border:none;position:relative;}/*!sc*/ 131.dhCohm hr::after{content:'· · ·';text-align:center;width:100%;display:block;color:#999;letter-spacing:5px;user-select:none;}/*!sc*/ 132data-styled.g35[id="styled__Content-sc-g87c0z-8"]{content:"dhCohm,"}/*!sc*/ 133</style><style>.gatsby-image-wrapper{position:relative;overflow:hidden}.gatsby-image-wrapper picture.object-fit-polyfill{position:static!important}.gatsby-image-wrapper img{bottom:0;height:100%;left:0;margin:0;max-width:none;padding:0;position:absolute;right:0;top:0;width:100%;object-fit:cover}.gatsby-image-wrapper [data-main-image]{opacity:0;transform:translateZ(0);transition:opacity .25s linear;will-change:opacity}.gatsby-image-wrapper-constrained{display:inline-block;vertical-align:top}</style><noscript><style>.gatsby-image-wrapper noscript [data-main-image]{opacity:1!important}.gatsby-image-wrapper [data-placeholder-image]{opacity:0!important}</style></noscript>
133<script type="module">const e="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;e&&document.body.addEventListener("load",(function(e){const t=e.target;if(void 0===t.dataset.mainImage)return;if(void 0===t.dataset.gatsbyImageSsr)return;let a=null,n=t;for(;null===a&&n;)void 0!==n.parentNode.dataset.gatsbyImageWrapper&&(a=n.parentNode),n=n.parentNode;const o=a.querySelector("[data-placeholder-image]"),r=new Image;r.src=t.currentSrc,r.decode().catch((()=>{})).then((()=>{t.style.opacity=1,o&&(o.style.opacity=0,o.style.transition="opacity 500ms linear")}))}),!0);</script>
133<style type="text/css"> 134 .anchor.before { 135 position: absolute; 136 top: 0; 137 left: 0; 138 transform: translateX(-100%); 139 padding-right: 4px; 140 } 141 .anchor.after { 142 display: inline-block; 143 padding-left: 4px; 144 } 145 h1 .anchor svg, 146 h2 .anchor svg, 147 h3 .anchor svg, 148 h4 .anchor svg, 149 h5 .anchor svg, 150 h6 .anchor svg { 151 visibility: hidden; 152 } 153 h1:hover .anchor svg, 154 h2:hover .anchor svg, 155 h3:hover .anchor svg, 156 h4:hover .anchor svg, 157 h5:hover .anchor svg, 158 h6:hover .anchor svg, 159 h1 .anchor:focus svg, 160 h2 .anchor:focus svg, 161 h3 .anchor:focus svg, 162 h4 .anchor:focus svg, 163 h5 .anchor:focus svg, 164 h6 .anchor:focus svg { 165 visibility: visible; 166 } 167 </style>
167<script> 168 document.addEventListener("DOMContentLoaded", function(event) { 169 var hash = window.decodeURI(location.hash.replace('#', '')) 170 if (hash !== '') { 171 var element = document.getElementById(hash) 172 if (element) { 173 var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop 174 var clientTop = document.documentElement.clientTop || document.body.clientTop || 0 175 var offset = element.getBoundingClientRect().top + scrollTop - clientTop 176 // Wait for the browser to finish rendering before scrolling. 177 setTimeout((function() { 178 window.scrollTo(0, offset - 0) 179 }), 0) 180 } 181 } 182 }) 183 </script>
183<script src="https://cdn.usefathom.com/script.js" data-site="EIKSECOJ" data-excluded-domains="localhost" defer=""></script>
183<script src="https://unpkg.com/[email protected]/dist/quicklink.umd.js" defer=""></script>
183<script> 184 window.addEventListener('load', () => quicklink.listen()); 185 </script>
185</head><body><div id="___gatsby"><div style="outline:none" tabindex="-1" id="gatsby-focus-wrapper"><!--$--><div><div class="WidthWrapper-sc-1hg7f1n-0 fHUXzs"><div class="styled__Container-sc-1wdhdtf-0 jLAQgJ styled__Nav-sc-g87c0z-0 jkrHkn"><a href="/" class="styled__BlockLink-sc-1wdhdtf-4 hruSyn"><div><svg class="styled__LogoSvg-sc-lzfaox-0 jLbNpk styled__Logo-sc-1wdhdtf-1 hMAVWe js--site-logo" width="53.445835mm" height="53.445835mm" viewBox="0 0 53.445835 53.445835" version="1.1" id="svg8"><g transform="translate(-45.848515,-91.116368)"><rect style="fill:none;stroke-width:2.64583349;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="rect815" width="50.800003" height="50.800003" x="47.171432" y="92.439285"></rect><g aria-label="PERF PERF PERF" style="fill:none;font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill-opacity:1;stroke:none;stroke-width:0.26458332" id="perf-letters"><path d="m 61.316555,100.90596 q 0.98425,0 1.703916,0.32808 0.73025,0.32809 1.121834,0.93134 0.391583,0.60325 0.391583,1.42875 0,0.81491 -0.391583,1.42875 -0.391584,0.60325 -1.121834,0.93133 -0.719666,0.3175 -1.703916,0.3175 h -1.49225 v 2.04258 h -1.7145 v -7.40833 z m -0.09525,3.96875 q 0.772583,0 1.17475,-0.32808 0.402166,-0.33867 0.402166,-0.9525 0,-0.62442 -0.402166,-0.9525 -0.402167,-0.33867 -1.17475,-0.33867 h -1.397 v 2.57175 z" style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:1;font-family:Montserrat;letter-spacing:0.26458332px;fill-opacity:1;stroke-width:0.26458332" id="path842"></path><path d="m 71.758666,106.93846 v 1.37583 H 66.0225 v -7.40833 h 5.598583 v 1.37583 h -3.894667 v 1.60867 H 71.166 v 1.3335 h -3.439584 v 1.7145 z" style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:1;font-family:Montserrat;letter-spacing:0.26458332px;fill-opacity:1;stroke-width:0.26458332" id="path844"></path><path d="m 78.117927,108.31429 -1.42875,-2.06375 h -0.08467 -1.49225 v 2.06375 h -1.7145 v -7.40833 h 3.20675 q 0.98425,0 1.703917,0.32808 0.73025,0.32809 1.121833,0.93134 0.391583,0.60325 0.391583,1.42875 0,0.8255 -0.402166,1.42875 -0.391584,0.59266 -1.121834,0.91016 l 1.661583,2.38125 z m -0.03175,-4.72016 q 0,-0.62442 -0.402167,-0.9525 -0.402167,-0.33867 -1.17475,-0.33867 h -1.397 v 2.58233 h 1.397 q 0.772583,0 1.17475,-0.33866 0.402167,-0.33867 0.402167,-0.9525 z" style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:1;font-family:Montserrat;letter-spacing:0.26458332px;fill-opacity:1;stroke-width:0.26458332" id="path846"></path><path d="m 83.148979,102.28179 v 1.95792 h 3.429 v 1.37583 h -3.429 v 2.69875 h -1.7145 v -7.40833 h 5.598584 v 1.37583 z" style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:1;font-family:Montserrat;letter-spacing:0.26458332px;fill-opacity:1;stroke-width:0.26458332" id="path848"></path><path d="m 61.316555,114.13513 q 0.98425,0 1.703916,0.32808 0.73025,0.32808 1.121834,0.93133 0.391583,0.60325 0.391583,1.42875 0,0.81492 -0.391583,1.42875 -0.391584,0.60325 -1.121834,0.93134 -0.719666,0.3175 -1.703916,0.3175 h -1.49225 v 2.04258 h -1.7145 v -7.40833 z m -0.09525,3.96875 q 0.772583,0 1.17475,-0.32809 0.402166,-0.33866 0.402166,-0.9525 0,-0.62441 -0.402166,-0.9525 -0.402167,-0.33866 -1.17475,-0.33866 h -1.397 v 2.57175 z" style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:1;font-family:Montserrat;letter-spacing:0.26458332px;fill-opacity:1;stroke-width:0.26458332" id="path850"></path><path d="m 71.758666,120.16763 v 1.37583 H 66.0225 v -7.40833 h 5.598583 v 1.37583 h -3.894667 v 1.60867 H 71.166 v 1.3335 h -3.439584 v 1.7145 z" style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:1;font-family:Montserrat;letter-spacing:0.26458332px;fill-opacity:1;stroke-width:0.26458332" id="path852"></path><path d="m 78.117927,121.54346 -1.42875,-2.06375 h -0.08467 -1.49225 v 2.06375 h -1.7145 v -7.40833 h 3.20675 q 0.98425,0 1.703917,0.32808
1850.73025,0.32808 1.121833,0.93133 0.391583,0.60325 0.391583,1.42875 0,0.8255 -0.402166,1.42875 -0.391584,0.59267 -1.121834,0.91017 l 1.661583,2.38125 z m -0.03175,-4.72017 q 0,-0.62441 -0.402167,-0.9525 -0.402167,-0.33866 -1.17475,-0.33866 h -1.397 v 2.58233 h 1.397 q 0.772583,0 1.17475,-0.33867 0.402167,-0.33866 0.402167,-0.9525 z" style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:1;font-family:Montserrat;letter-spacing:0.26458332px;fill-opacity:1;stroke-width:0.26458332" id="path854"></path><path d="m 83.148979,115.51096 v 1.95792 h 3.429 v 1.37583 h -3.429 v 2.69875 h -1.7145 v -7.40833 h 5.598584 v 1.37583 z" style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:1;font-family:Montserrat;letter-spacing:0.26458332px;fill-opacity:1;stroke-width:0.26458332" id="path856"></path><path d="m 61.316555,127.36429 q 0.98425,0 1.703916,0.32809 0.73025,0.32808 1.121834,0.93133 0.391583,0.60325 0.391583,1.42875 0,0.81492 -0.391583,1.42875 -0.391584,0.60325 -1.121834,0.93133 -0.719666,0.3175 -1.703916,0.3175 h -1.49225 v 2.04259 h -1.7145 v -7.40834 z m -0.09525,3.96875 q 0.772583,0 1.17475,-0.32808 0.402166,-0.33867 0.402166,-0.9525 0,-0.62442 -0.402166,-0.9525 -0.402167,-0.33867 -1.17475,-0.33867 h -1.397 v 2.57175 z" style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:1;font-family:Montserrat;letter-spacing:0.26458332px;fill-opacity:1;stroke-width:0.26458332" id="path858"></path><path d="m 71.758666,133.39679 v 1.37584 H 66.0225 v -7.40834 h 5.598583 v 1.37584 h -3.894667 v 1.60866 H 71.166 v 1.3335 h -3.439584 v 1.7145 z" style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:1;font-family:Montserrat;letter-spacing:0.26458332px;fill-opacity:1;stroke-width:0.26458332" id="path860"></path><path d="m 78.117927,134.77263 -1.42875,-2.06375 h -0.08467 -1.49225 v 2.06375 h -1.7145 v -7.40834 h 3.20675 q 0.98425,0 1.703917,0.32809 0.73025,0.32808 1.121833,0.93133 0.391583,0.60325 0.391583,1.42875 0,0.8255 -0.402166,1.42875 -0.391584,0.59267 -1.121834,0.91017 l 1.661583,2.38125 z m -0.03175,-4.72017 q 0,-0.62442 -0.402167,-0.9525 -0.402167,-0.33867 -1.17475,-0.33867 h -1.397 v 2.58234 h 1.397 q 0.772583,0 1.17475,-0.33867 0.402167,-0.33867 0.402167,-0.9525 z" style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:1;font-family:Montserrat;letter-spacing:0.26458332px;fill-opacity:1;stroke-width:0.26458332" id="path862"></path><path d="m 83.148979,128.74013 v 1.95791 h 3.429 v 1.37584 h -3.429 v 2.69875 h -1.7145 v -7.40834 h 5.598584 v 1.37584 z" style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:1;font-family:Montserrat;letter-spacing:0.26458332px;fill-opacity:1;stroke-width:0.26458332" id="path864"></path></g></g></svg></div></a><div class="styled__ItemsWrapper-sc-1wdhdtf-2 cmWeoQ"><div class="styled__Items-sc-1wdhdtf-3 IrpCG"><a href="/">Talks and Case Studies</a><a href="/workshops">Workshops</a><div class="styled__SecondaryItems-sc-1wdhdtf-5 PaASP"><a href="mailto:[email protected]">[email protected]</a></div></div></div></div><header class="styled__Header-sc-g87c0z-1 hjUmKx"><h1 class="styled__Title-sc-g87c0z-4 gCDoRG">Case Study: Making React Interactions in Causal 4à Faster</h1><div class="styled__TopMeta-sc-g87c0z-2 fpALeQ">19 min read<!-- --> · Author:<!-- --> <a href="https://twitter.com/iamakulov" rel="author">Ivan Akulov</a></div></header><article class="styled__Content-sc-g87c0z-8 dhCohm"><div><p><a href="https://www.causal.app/">Causal</a> is a React spreadsheet app for creating complex forecasting models:</p> 186<figure class="media-container"><picture><img src="/static/4140e21ab41fdfe01834c17af6326ccb/e7448/image0.png" srcset="/static/4140e21ab41fdfe01834c17af6326ccb/0608e/image0.png 225w, 187/static/4140e21ab41fdfe01834c17af6326ccb/31401/image0.png 450w, 188/static/4140e21ab41fdfe01834c17af6326ccb/e7448/image0.png 900w, 189/static/4140e21ab41fdfe01834c17af6326ccb/9ac8b/image0.png 1350w, 190/static/4140e21ab41fdfe01834c17af6326ccb/28bdc/image0.png 1800w, 191/static/4140e21ab41fdfe01834c17af6326ccb/6bde6/image0.png 2100w" alt="" width="900" height="587"></picture></figure> 192<p>Sometimes, Causal models get huge, which raises challenges in keeping them fast. One of these challenges is UI interactions. For example, in a huge model, if you opened the Categories pane and tried to fill several values in a row, youâd notice how the UI gets pretty laggy:</p> 193<figure class="media-container"><video controls muted style="aspect-ratio: 2880 / 1800"><source src="/dce6bed3dc8117fcdd069c4051fb9efe/image1.mp4" type="video/mp4"></video><figcaption class="media-container__caption">It might be hard to see when the app is laggy in this video. To make it easier, keep an eye on the âFrame Rateâ popup in the top left corner â when itâs red or yellow, the main thread is frozen. (By the way, this is a built-in Chrome DevTools feature! You could enable it in DevTools â More Tools â Rendering â Frame Rendering Stats.)</figcaption></figure> 194<p>Hereâs how PerfPerfPerf helped Causal to speed up this interaction by almost 4à â with nearly every optimization changing just a few lines.</p>
195<nav class="toc toc_with-header"><h1 class="toc__header">Contents</h1><ul> 196<li> 197<p><a href="#profiling-the-interaction">Profiling The Interaction</a></p> 198</li> 199<li> 200<p><a href="#ag-grid-fixing-an-extra-render">AG Grid: Fixing An Extra Render</a></p> 201</li> 202<li> 203<p><a href="#ag-grid-removing-the-renders">AG Grid: Removing The Renders</a></p> 204</li> 205<li> 206<p><a href="#running-useeffect-less-often">Running <code class="language-text">useEffect</code> Less Often</a></p> 207</li> 208<li> 209<p><a href="#deep-areequal">Deep <code class="language-text">areEqual</code></a></p> 210</li> 211<li> 212<p><a href="#whats-still-in-the-works">Whatâs Still In The Works</a></p> 213<ul> 214<li><a href="#more-granular-updates">More Granular Updates</a></li> 215<li><a href="#useselector-vs-usestore"><code class="language-text">useSelector</code> vs <code class="language-text">useStore</code></a></li> 216</ul> 217</li> 218<li> 219<p><a href="#what-didnt-work">What Didnât Work</a></p> 220<ul> 221<li><a href="#web-workers">Web Workers</a></li> 222</ul> 223</li> 224<li> 225<p><a href="#results">Results</a></p> 226</li> 227</ul></nav> 228<h1 id="profiling-the-interaction" style="position:relative;"><a href="#profiling-the-interaction" aria-label="profiling the interaction permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Profiling The Interaction</h1> 229<p>To optimize the interaction, we need to figure out what makes it slow. My go-to tool for that is Chrome DevTools:</p> 230<figure class="media-container"><picture><img src="/static/91a7874262a4b3ad1881ab6b4051a3ee/e7448/image2.png" srcset="/static/91a7874262a4b3ad1881ab6b4051a3ee/0608e/image2.png 225w, 231/static/91a7874262a4b3ad1881ab6b4051a3ee/31401/image2.png 450w, 232/static/91a7874262a4b3ad1881ab6b4051a3ee/e7448/image2.png 900w, 233/static/91a7874262a4b3ad1881ab6b4051a3ee/9ac8b/image2.png 1350w, 234/static/91a7874262a4b3ad1881ab6b4051a3ee/28bdc/image2.png 1800w, 235/static/91a7874262a4b3ad1881ab6b4051a3ee/7f509/image2.png 2648w" alt="" width="900" height="589"></picture><figcaption class="media-container__caption">Open Chrome DevTools â Performance. Click record. Update one value. Wait a bit, and stop the recording.</figcaption></figure> 236<p>Thereâs a lot of stuff in the recording, so it might be confusing if youâre seeing it for the first time. But thatâs okay! What we need to pay attention to is just two areas:</p> 237<figure class="media-container"><picture><img src="/static/16879990769d79f18cc065fd38e7ebd9/e7448/image3.png" srcset="/static/16879990769d79f18cc065fd38e7ebd9/0608e/image3.png 225w, 238/static/16879990769d79f18cc065fd38e7ebd9/31401/image3.png 450w, 239/static/16879990769d79f18cc065fd38e7ebd9/e7448/image3.png 900w, 240/static/16879990769d79f18cc065fd38e7ebd9/9ac8b/image3.png 1350w, 241/static/16879990769d79f18cc065fd38e7ebd9/28bdc/image3.png 1800w, 242/static/16879990769d79f18cc065fd38e7ebd9/7f509/image3.png 2648w" alt="" width="900" height="589"></picture><figcaption class="media-container__caption">The top area (the CPU row) shows when the page was busy. The bottom area (the Main pane) shows <em>why</em> the page was busy.</figcaption></figure> 243<p>So whatâs going on here? If you go through the recording and click through a bunch of rectangles, youâll notice some patterns:</p> 244<ul class="list_spread"> 245<li> 246<p><strong>There are a lot of React renders.</strong> Specifically, every rectangle called <code class="language-text">performSyncWorkOnRoot</code> is (roughly) a function that starts a React render cycle. And there are lots of them (precisely, 1325):</p> 247<figure class="media-container"><picture><img src="/static/886cab4db7f5927548c95a364751a8e3/e7448/image4.png" srcset="/static/886cab4db7f5927548c95a364751a8e3/0608e/image4.png 225w, 248/static/886cab4db7f5927548c95a364751a8e3/31401/image4.png 450w, 249/static/886cab4db7f5927548c95a364751a8e3/e7448/image4.png 900w, 250/static/886cab4db7f5927548c95a364751a8e3/9ac8b/image4.png 1350w, 251/static/886cab4db7f5927548c95a364751a8e3/28bdc/image4.png 1800w, 252/static/886cab4db7f5927548c95a364751a8e3/7f509/image4.png 2648w" alt="" width="900" height="589"></picture><figcaption class="media-container__caption">To search for a function name, press â+F (or, if not using macOS, Ctrl+F). DevTools will highlight the first found match, and you can jump to the next ones.</figcaption></figure> 253</li> 254<li> 255<p><strong>Most of these React renders are caused by AG Grid.</strong> <a href="https://www.ag-grid.com/">AG Grid</a> is a library that Causal uses to render tables/grids:</p> 256<figure class="media-container"><picture><img src="/static/ec5ce86ae6eca13b954e3c4e5422660c/e7448/image5.png" srcset="/static/ec5ce86ae6eca13b954e3c4e5422660c/0608e/image5.png 225w, 257/static/ec5ce86ae6eca13b954e3c4e5422660c/31401/image5.png 450w, 258/static/ec5ce86ae6eca13b954e3c4e5422660c/e7448/image5.png 900w, 259/static/ec5ce86ae6eca13b954e3c4e5422660c/9ac8b/image5.png 1350w, 260/static/ec5ce86ae6eca13b954e3c4e5422660c/28bdc/image5.png 1800w, 261/static/ec5ce86ae6eca13b954e3c4e5422660c/1ce0c/image5.png 2762w" alt="" width="900" height="603"></picture></figure> 262<p>If you find any <code class="language-text">
262performSyncWorkOnRoot</code> rectangle and then scroll up, youâll see what caused that function to run (meaning, caused that React render). Most of the time, that will be some AG Grid code:</p> 263<figure class="media-container"><picture><img src="/static/29e92ec5bea52632c5c5e885db2bda5f/e7448/image6.png" srcset="/static/29e92ec5bea52632c5c5e885db2bda5f/0608e/image6.png 225w, 264/static/29e92ec5bea52632c5c5e885db2bda5f/31401/image6.png 450w, 265/static/29e92ec5bea52632c5c5e885db2bda5f/e7448/image6.png 900w, 266/static/29e92ec5bea52632c5c5e885db2bda5f/9ac8b/image6.png 1350w, 267/static/29e92ec5bea52632c5c5e885db2bda5f/28bdc/image6.png 1800w, 268/static/29e92ec5bea52632c5c5e885db2bda5f/7f509/image6.png 2648w" alt="" width="900" height="1226"></picture></figure> 269</li> 270<li> 271<p><strong>Some of the code runs several times.</strong> E.g., at the beginning of the recording, we call <code class="language-text">GridApi.refreshServerSide</code> and <code class="language-text">GridApi.refreshCells</code> two times in a row:</p> 272<figure class="media-container"><picture><img src="/static/f7926ee2daf7326149916289ede5fcc1/e7448/image7.png" srcset="/static/f7926ee2daf7326149916289ede5fcc1/0608e/image7.png 225w, 273/static/f7926ee2daf7326149916289ede5fcc1/31401/image7.png 450w, 274/static/f7926ee2daf7326149916289ede5fcc1/e7448/image7.png 900w, 275/static/f7926ee2daf7326149916289ede5fcc1/9ac8b/image7.png 1350w, 276/static/f7926ee2daf7326149916289ede5fcc1/28bdc/image7.png 1800w, 277/static/f7926ee2daf7326149916289ede5fcc1/7f509/image7.png 2648w" alt="" width="900" height="875"></picture></figure> 278<p>Later, some code seems to call <code class="language-text">getRows</code> over and over and over again:</p> 279<figure class="media-container"><picture><img src="/static/6a744d148fc7f3c498f0ccc86d2edb19/e7448/image8.png" srcset="/static/6a744d148fc7f3c498f0ccc86d2edb19/0608e/image8.png 225w, 280/static/6a744d148fc7f3c498f0ccc86d2edb19/31401/image8.png 450w, 281/static/6a744d148fc7f3c498f0ccc86d2edb19/e7448/image8.png 900w, 282/static/6a744d148fc7f3c498f0ccc86d2edb19/9ac8b/image8.png 1350w, 283/static/6a744d148fc7f3c498f0ccc86d2edb19/28bdc/image8.png 1800w, 284/static/6a744d148fc7f3c498f0ccc86d2edb19/7f509/image8.png 2648w" alt="" width="900" height="875"></picture></figure> 285<p>This is good! When we have some code that runs ten times in a row, we can improve a single run â and get a 10Ã improvement. And if some of these runs end up being unnecessary, weâll be able to remove them altogether.</p> 286</li> 287</ul> 288<p>Letâs dive in.</p> 289<h1 id="ag-grid-fixing-an-extra-render" style="position:relative;"><a href="#ag-grid-fixing-an-extra-render" aria-label="ag grid fixing an extra render permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>AG Grid: Fixing An Extra Render</h1> 290<p>Across the recording, four chunks of JavaScript start with <code class="language-text">GridApi.refreshServerSide</code>:</p> 291<figure class="media-container"><picture><img src="/static/7c70def9495ce7fe9c0419b06b96f9e0/e7448/image9.png" srcset="/static/7c70def9495ce7fe9c0419b06b96f9e0/0608e/image9.png 225w, 292/static/7c70def9495ce7fe9c0419b06b96f9e0/31401/image9.png 450w, 293/static/7c70def9495ce7fe9c0419b06b96f9e0/e7448/image9.png 900w, 294/static/7c70def9495ce7fe9c0419b06b96f9e0/9ac8b/image9.png 1350w, 295/static/7c70def9495ce7fe9c0419b06b96f9e0/28bdc/image9.png 1800w, 296/static/7c70def9495ce7fe9c0419b06b96f9e0/7f509/image9.png 2648w" alt="" width="900" height="875"></picture></figure> 297<div class="sidenote"><div class="sidenote__body"><p>Down the flame chart, these chunks of JavaScript cause many React rerenders. To figure out which components are being rendered, letâs scroll down and hunt for component names.</p></div><blockquote class="sidenote__heading"><strong class="sidenote__remark">Sidenote:</strong> Hunting for component names works because to render a functional component, React just calls it.
297(For class components, React calls the <code class="language-text">.render()</code> method.)</blockquote></div> 298<figure class="media-container media-container_scrollable"><picture><img src="/static/19ecc9023f732abfc26e971b4ad9161c/e7448/image10.png" srcset="/static/19ecc9023f732abfc26e971b4ad9161c/0608e/image10.png 225w, 299/static/19ecc9023f732abfc26e971b4ad9161c/31401/image10.png 450w, 300/static/19ecc9023f732abfc26e971b4ad9161c/e7448/image10.png 900w, 301/static/19ecc9023f732abfc26e971b4ad9161c/9ac8b/image10.png 1350w, 302/static/19ecc9023f732abfc26e971b4ad9161c/28bdc/image10.png 1800w, 303/static/19ecc9023f732abfc26e971b4ad9161c/2e0b9/image10.png 3161w" alt="" width="900" height="632" style="min-height: 600px; min-width: calc(600px * 1.4240506329113924);"></picture></figure> 304<blockquote class="note"><p><strong>Why not use React Profiler?</strong> Another way to see which components are rendered is to record a trace <a href="https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en">in React Profiler</a>. However, when you have a lot of rerenders, matching that trace with the DevTools Performance trace is hard â and if you make a mistake, youâll end up optimizing the wrong rerenders.</p></blockquote> 305<p>If you click through the component names in the recording, youâll realize every component is <code class="language-text">RowContainerComp</code>. This is <a href="https://github.com/ag-grid/ag-grid/blob/760be3ac7b86716780f58645e8113720f2f63bc2/community-modules/react/src/reactUi/rows/rowContainerComp.tsx">a component from AG Grid</a>:</p> 306<figure class="media-container"><picture><img src="/static/3905719a5f54cf73a2e70ee66ddeb1a4/e7448/image11.png" srcset="/static/3905719a5f54cf73a2e70ee66ddeb1a4/0608e/image11.png 225w, 307/static/3905719a5f54cf73a2e70ee66ddeb1a4/31401/image11.png 450w, 308/static/3905719a5f54cf73a2e70ee66ddeb1a4/e7448/image11.png 900w, 309/static/3905719a5f54cf73a2e70ee66ddeb1a4/9ac8b/image11.png 1350w, 310/static/3905719a5f54cf73a2e70ee66ddeb1a4/28bdc/image11.png 1800w, 311/static/3905719a5f54cf73a2e70ee66ddeb1a4/7f509/image11.png 2648w" alt="" width="900" height="629"></picture></figure> 312<p>Why do these components render? To answer that, letâs <a href="https://twitter.com/iamakulov/status/1417122098267312133">switch to React Profiler</a> and find these components there:</p> 313<figure class="media-container"><picture><img src="/static/cccbf6cfeafa9612a9a2faeb23cbcce2/e7448/image12.png" srcset="/static/cccbf6cfeafa9612a9a2faeb23cbcce2/0608e/image12.png 225w, 314/static/cccbf6cfeafa9612a9a2faeb23cbcce2/31401/image12.png 450w, 315/static/cccbf6cfeafa9612a9a2faeb23cbcce2/e7448/image12.png 900w, 316/static/cccbf6cfeafa9612a9a2faeb23cbcce2/9ac8b/image12.png 1350w, 317/static/cccbf6cfeafa9612a9a2faeb23cbcce2/28bdc/image12.png 1800w, 318/static/cccbf6cfeafa9612a9a2faeb23cbcce2/78f36/image12.png 2056w" alt="{caption:"Open the React Profiler and <a href="https://share.cleanshot.com/ICgcZg">enable âRecord why each component rendered while profiling.â</a> Then, click âRecordâ â update a variable â wait a bit â stop the recording â find any <code>RowContainerComp</code> in the recording. This isnât perfect (you might pick up a wrong rerender) but is mostly precise."}" width="900" height="693"></picture></figure> 319<blockquote class="note"><p><strong>Why use React Profiler this time?</strong> This time, we <em>are</em> using React Profiler. Weâve learned the component names, so we donât need to match the trace with the DevTools performance pane anymore.</p><p>Other ways to learn why a component rerenders are <a href="https://github.com/welldone-software/why-did-you-render">why-did-you-render</a> and <a href="https://usehooks.com/useWhyDidYouUpdate/">useWhyDidYouUpdate</a>. They work great with first-party code but are harder to configure for third-party one (like AG Grid components).</p></blockquote> 320<p>As we see, <code class="language-text">RowContainerComp</code> components rerender because their hook 2 changed. To find that hook, letâs switch from the Profiler to the Components pane â and match component hooks <a href="https://github.com/ag-grid/ag-grid/blob/760be3ac7b86716780f58645e8113720f2f63bc2/community-modules/react/src/reactUi/rows/rowContainerComp.tsx">with the source code</a>:</p> 321<figure class="media-container media-container_scrollable"><picture><img src="/static/1cc6443fbbb43607a1fdb0f848ff3ad3/e7448/image13.png" srcset="/static/1cc6443fbbb43607a1fdb0f848ff3ad3/0608e/image13.png 225w, 322/static/1cc6443fbbb43607a1fdb0f848ff3ad3/31401/image13.png 450w, 323/static/1cc6443fbbb43607a1fdb0f848ff3ad3/e7448/image13.png 900w, 324/static/1cc6443fbbb43607a1fdb0f848ff3ad3/9ac8b/image13.png 1350w, 325/static/1cc6443fbbb43607a1fdb0f848ff3ad3/28bdc/image13.png 1800w, 326/static/1cc6443fbbb43607a1fdb0f848ff3ad3/f9c26/image13.png 2880w" alt="" width="900" height="563" style="min-height: 600px; min-width: calc(600px * 1.5985790408525755);"></picture></figure> 327<p>Hook no. 2 is this:</p> 328<div class="gatsby-highlight" data-language="ts"><pre class="language-ts"><code class="language-ts"><span class="token keyword">const</span> <span class="token punctuation">[</span>rowCtrlsOrdered<span class="token punctuation">,</span> setRowCtrlsOrdered<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token generic-function"><span class="token function">useState</span><span class="token generic class-name"><span class="token operator"><</span>RowCtrl<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token operator">></span></span></span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div> 329<blockquote class="note"><p><strong>Why wonât we just count hooks in the source code?</strong> Thatâs the most obvious approach, but it rarely works. Thatâs because:</p><ul class="list_spread"> 330<li> 331<p>React skips <code class="language-text">useContext</code> when counting hooks. (This is probably because <code class="language-text">useContext</code> <a href="https://twitter.com/acdlite/status/1586779571290275840">is implemented differently from other hooks</a>.)</p> 332</li> 333<li> 334<p>React doesnât keep track of custom hooks. Instead, it counts every built-in hook inside a custom hook (except <code class="language-text">useContext</code>). For example, if a component calls <a href="https://react-redux.js.org/api/hooks#useselector"><code class="language-text">useSelector</code> from Redux</a>, and <code class="language-text">useSelector</code> uses four React hooks inside, React profiler might show you âHook 3 changed.â</p> 335</li> 336</ul></blockquote> 337<p>So, we figured out that our interaction renders a bunch of <code class="language-text">RowContainerComp</code> components from AG Grid. These components rerender because their hooks no. 2 (the <code class="language-text">rowCtrlsOrdered</code> state) change. Now, if you look through the componentâs source code, youâll notice that <code class="language-text">rowCtrlsOrdered</code> is updated inside a <code class="language-text">useEffect</code>:</p> 338<figure class="media-container"><picture><img src="/static/a106692087210f0139ae44228102f6b6/e7448/image14.png" srcset="/static/a106692087210f0139ae44228102f6b6/0608e/image14.png 225w, 339/static/a106692087210f0139ae44228102f6b6/31401/image14.png 450w, 340/static/a106692087210f0139ae44228102f6b6/e7448/image14.png 900w, 341/static/a106692087210f0139ae44228102f6b6/9ac8b/image14.png 1350w, 342/static/a106692087210f0139ae44228102f6b6/28bdc/image14.png 1800w, 343/static/a106692087210f0139ae44228102f6b6/228fd/image14.png 2272w" alt="" width="900" height="697"></picture></figure> 344<p>And that <code class="language-text">useEffect</code> triggers when the <code class="language-text">rowCtrls</code> or the <code class="language-text">domOrder</code> state changes:</p> 345<figure class="media-container"><picture><img src="/static/36d93fd3a9149200dfd2c23e45f659e5/e7448/image15.png" srcset="/static/36d93fd3a9149200dfd2c23e45f659e5/0608e/image15.png 225w, 346/static/36d93fd3a9149200dfd2c23e45f659e5/31401/image15.png 450w, 347/static/36d93fd3a9149200dfd2c23e45f659e5/e7448/image15.png 900w, 348/static/36d93fd3a9149200dfd2c23e45f659e5/9ac8b/image15.png 1350w, 349/static/36d93fd3a9149200dfd2c23e45f659e5/28bdc/image15.png 1800w, 350/static/36d93fd3a9149200dfd2c23e45f659e5/228fd/image15.png 2272w" alt="" width="900" height="697"></picture></figure> 351<p>This is not optimal! AG Grid is setting state from inside a <code class="language-text">useEffect</code>. This means it schedules a new update right after another one has happened. Hereâs the entire order of events:</p> 352<figure class="media-container"><picture><img src="/static/118de66b4842faefdf36df4a2811433c/e7448/image16.png" srcset="/static/118de66b4842faefdf36df4a2811433c/0608e/image16.png 225w, 353/static/118de66b4842faefdf36df4a2811433c/31401/image16.png 450w, 354/static/118de66b4842faefdf36df4a2811433c/e7448/image16.png 900w, 355/static/118de66b4842faefdf36df4a2811433c/9ac8b/image16.png 1350w, 356/static/118de66b4842faefdf36df4a2811433c/28bdc/image16.png 1800w, 357/static/118de66b4842faefdf36df4a2811433c/b76a9/image16.png 3104w" alt="" width="900" height="572"></picture></figure> 358<ol class="list_spread"> 359<li> 360<p>When the component mounts, it exposes several functions to the AG Grid core</p> 361</li> 362<li> 363<p>Later, AG Grid calls <code class="language-text">compProxy.setRowCtrls</code></p> 364</li> 365<li> 366<p><code class="language-text">compProxy.setRowCtrls</code> updates the <code class="language-text">rowCtrls</code> state</p> 367</li> 368<li> 369<p>Because the state changed, the component rerenders ð¥</p> 370</li> 371<li> 372<p>The <code class="language-text">rowCtrls</code> state got updated, so React runs <code class="language-text">useEffect</code></p> 373</li> 374<li> 375<p>Inside <code class="language-text">useEffect</code>, React updates the <code class="language-text">rowCtrlsOrdered</code> state</p> 376</li> 377<li> 378<p>Because the state changed, the component rerenders again ð¥</p> 379</li> 380</ol> 381<p>Weâre rerendering (ð¥) the component twice just to update hook no. 2! This isnât great. If AG Grid updated <code class="language-text">rowCtrlsOrdered</code> immediately at step 2 instead of 5, weâd be able to avoid an extra render.</p> 382<div class="sidenote"><div class="sidenote__body"><p>So why donât we make AG Grid do this? Using <a href="https://yarnpkg.com/cli/patch"><code class="language-text">yarn patch</code></a>, letâs patch the <code class="language-text">@ag-grid-community/react</code> package to eliminate the extra render:</p></div><blockquote class="sidenote__heading"><strong class="sidenote__remark">Sidenote:</strong> With npm, <a href="https://www.npmjs.com/package/patch-package"><code class="language-text">patch-package</code></a> works just as well.</blockquote></div> 383<figure class="media-container"><picture><img src="/static/d60583b8c09f72ec04a0157d769c5f96/e7448/image17.png" srcset="/static/d60583b8c09f72ec04a0157d769c5f96/0608e/image17.png 225w, 384/static/d60583b8c09f72ec04a0157d769c5f96/31401/image17.png 450w, 385/static/d60583b8c09f72ec04a0157d769c5f96/e7448/image17.png 900w, 386/static/d60583b8c09f72ec04a0157d769c5f96/9ac8b/image17.png 1350w, 387/static/d60583b8c09f72ec04a0157d769c5f96/28bdc/image17.png 1800w, 388/static/d60583b8c09f72ec04a0157d769c5f96/b76a9/image17.png 3104w" alt="" width="900" height="738"></picture><figcaption class="media-container__caption"><a href='https://gist.github.com/6762c0e25368d57bb8b8bd334a14cd6d'>Full patch</a>. Weâve notified the AG Grid team â unfortunately, theyâre not accepting PRs from the community.</figcaption></figure> 389<p>This alone cuts the number of rerenders in half â and, because <code class="language-text">RowContainerComp</code> is rendered outside <code class="language-text">GridApi.refreshServerSide()</code> calls too, shaves off around 15-20% of the execution time.</p> 390<p>But weâre not done with AG Grid yet.</p> 391<h1 id="ag-grid-removing-the-renders" style="position:relative;"><a href="#ag-grid-removing-the-renders" aria-label="ag grid removing the renders permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>
391AG Grid: Removing The Renders</h1> 392<p>The <code class="language-text">RowContainerComp</code> components are containers for different parts of the grid:</p> 393<figure class="media-container"><picture><img src="/static/798b7924b407cf8384d479a10b2ce2e0/e7448/image18.png" srcset="/static/798b7924b407cf8384d479a10b2ce2e0/0608e/image18.png 225w, 394/static/798b7924b407cf8384d479a10b2ce2e0/31401/image18.png 450w, 395/static/798b7924b407cf8384d479a10b2ce2e0/e7448/image18.png 900w, 396/static/798b7924b407cf8384d479a10b2ce2e0/9ac8b/image18.png 1350w, 397/static/798b7924b407cf8384d479a10b2ce2e0/28bdc/image18.png 1800w, 398/static/798b7924b407cf8384d479a10b2ce2e0/f9c26/image18.png 2880w" alt="" width="900" height="563"></picture></figure> 399<p>These components render every time we type into the editor. We just removed half of these renders. But thereâs still another half, and itâs probably unnecessary â as nothing in these components changes visually.</p> 400<p>Whatâs causing these renders? As we learned in the previous section, <code class="language-text">RowContainerComp</code>s rerender when AG Grid calls <code class="language-text">compProxy.setRowCtrls</code>. In every call, AG Grid passes a new <code class="language-text">rowCtrls</code> array. Letâs add <a href="https://developer.chrome.com/blog/new-in-devtools-73/#logpoints">a logpoint</a> to see how the array looks:</p> 401<figure class="media-container"><picture><img src="/static/bc298a90a1c612362d60d3a81068e037/e7448/image19.png" srcset="/static/bc298a90a1c612362d60d3a81068e037/0608e/image19.png 225w, 402/static/bc298a90a1c612362d60d3a81068e037/31401/image19.png 450w, 403/static/bc298a90a1c612362d60d3a81068e037/e7448/image19.png 900w, 404/static/bc298a90a1c612362d60d3a81068e037/9ac8b/image19.png 1350w, 405/static/bc298a90a1c612362d60d3a81068e037/28bdc/image19.png 1800w, 406/static/bc298a90a1c612362d60d3a81068e037/d5be6/image19.png 2242w" alt="" width="900" height="650"></picture></figure> 407<p>and check the console output:</p> 408<figure class="media-container"><picture><img src="/static/09cf980fc6b647085dba635a1c51cb2d/e7448/image20.png" srcset="/static/09cf980fc6b647085dba635a1c51cb2d/0608e/image20.png 225w, 409/static/09cf980fc6b647085dba635a1c51cb2d/31401/image20.png 450w, 410/static/09cf980fc6b647085dba635a1c51cb2d/e7448/image20.png 900w, 411/static/09cf980fc6b647085dba635a1c51cb2d/9ac8b/image20.png 1350w, 412/static/09cf980fc6b647085dba635a1c51cb2d/28bdc/image20.png 1800w, 413/static/09cf980fc6b647085dba635a1c51cb2d/d5be6/image20.png 2242w" alt="" width="900" height="650"></picture></figure> 414<p>Woah, doesnât every log look the same?</p> 415<p>And indeed. If you debug this a bit, youâll realize that:</p> 416<ul class="list_compact"> 417<li>the array thatâs logged is always different (this is because AG Grid <a href="https://github.com/ag-grid/ag-grid/blob/760be3ac7b86716780f58645e8113720f2f63bc2/community-modules/core/src/ts/gridBodyComp/rowContainer/rowContainerCtrl.ts#L397-L399">is re-creating it with <code class="language-text">.filter()</code></a> before passing it in)</li> 418<li>however, all items in that array are identical (<code class="language-text">===</code>) across rerenders</li> 419</ul> 420<p>Inside the component, AG Grid never touches the array â it only maps its items. So, if the array items donât change, why should the component rerender?</p> 421<p>We can prevent this extra render by adding a shallow equality check:</p> 422<figure class="media-container"><picture><img src="/static/09aa94f344d17471f5e9e58b736001f1/e7448/image21.png" srcset="/static/09aa94f344d17471f5e9e58b736001f1/0608e/image21.png 225w, 423/static/09aa94f344d17471f5e9e58b736001f1/31401/image21.png 450w, 424/static/09aa94f344d17471f5e9e58b736001f1/e7448/image21.png 900w, 425/static/09aa94f344d17471f5e9e58b736001f1/9ac8b/image21.png 1350w, 426/static/09aa94f344d17471f5e9e58b736001f1/28bdc/image21.png 1800w, 427/static/09aa94f344d17471f5e9e58b736001f1/9f106/image21.png 2868w" alt="" width="900" height="619"></picture></figure> 428<p>This saves a lot of time. Because on every cell update, <code class="language-text">RowContainerComp</code> components rerender 1568 times (!), eliminating all renders cuts off <em>another</em> 15-30% of the total JS cost.</p> 429<h1 id="running-useeffect-less-often" style="position:relative;"><a href="#running-useeffect-less-often" aria-label="running useeffect less often permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Running <code class="language-text">useEffect</code> Less Often</h1> 430<p>Here are a few other parts of the recording:</p> 431<figure class="media-container"><picture><img src="/static/9fd722eab353b42fdaa3eb81b87c58ba/e7448/image22.png" srcset="/static/9fd722eab353b42fdaa3eb81b87c58ba/0608e/image22.png 225w, 432/static/9fd722eab353b42fdaa3eb81b87c58ba/31401/image22.png 450w, 433/static/9fd722eab353b42fdaa3eb81b87c58ba/e7448/image22.png 900w, 434/static/9fd722eab353b42fdaa3eb81b87c58ba/9ac8b/image22.png 1350w, 435/static/9fd722eab353b42fdaa3eb81b87c58ba/28bdc/image22.png 1800w, 436/static/9fd722eab353b42fdaa3eb81b87c58ba/7f509/image22.png 2648w" alt="" width="900" height="875"></picture></figure> 437<p>In these parts, we call a function called <code class="language-text">
437gridApi.refreshCells()</code>. This function gets called four times and, in total, takes around 5-10% of the JavaScript cost.</p> 438<p>Hereâs the Causal code that calls <code class="language-text">gridApi.refreshCells()</code>:</p> 439<div class="gatsby-highlight" data-language="php"><pre class="language-php"><code class="language-php"><span class="token comment">// â ï¸ Hacky:</span> 440<span class="token comment">// Hard refresh if autocomplete changes.</span> 441<span class="token comment">// This works around issue #XXX in the ShowFormulas view</span> 442<span class="token function">useEffect</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 443 <span class="token function">setTimeout</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 444 <span class="token comment">// Note: we're scheduling refreshCells() in a new task.</span> 445 <span class="token comment">// This ensures all previous AG Grid updates have time to propagate</span> 446 gridApi<span class="token operator">.</span><span class="token function">refreshCells</span><span class="token punctuation">(</span><span class="token punctuation">{</span> force<span class="token punctuation">:</span> <span class="token constant boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 447 <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 448<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>gridApi<span class="token punctuation">,</span> autocompleteVariables<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div> 449<p>This is an unfortunate hack (one of the few which every codebase has) that works around an issue with code editor autocomplete occasionally not picking up new variables.</p> 450<p>The workaround is supposed to run every time a new variable gets added or removed. However, currently, it runs way more frequently. Thatâs because <code class="language-text">autocompleteVariables</code> is a deeply nested object with a bunch of other information about variables, including their values:</p> 451<div class="gatsby-highlight" data-language="php"><pre class="language-php"><code class="language-php"><span class="token comment">// The `autocompleteVariables` object (simplified)</span> 452<span class="token punctuation">{</span> 453 <span class="token string double-quoted-string">"variable-id"</span><span class="token punctuation">:</span> <span class="token punctuation">{</span> 454 name<span class="token punctuation">:</span> <span class="token string double-quoted-string">"myVariable"</span><span class="token punctuation">,</span> 455 <span class="token argument-name">type</span><span class="token punctuation">:</span> <span class="token string double-quoted-string">"Variable"</span><span class="token punctuation">,</span> 456 <span class="token argument-name">dimensions</span><span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token operator">...</span><span class="token punctuation">]</span><span class="token punctuation">,</span> 457 <span class="token argument-name">model</span><span class="token punctuation">:</span> <span class="token operator">...</span><span class="token punctuation">,</span> 458 <span class="token punctuation">}</span><span class="token punctuation">,</span> 459 <span class="token operator">...</span> 460<span class="token punctuation">}</span></code></pre></div> 461<p>When you type in the cell, a few variables update their values. That causes <code class="language-text">autocompleteVariables</code> to update â and triggers a <code class="language-text">
461gridApi.refreshCells()</code> call. These calls are unnecessary â <code class="language-text">gridApi.refreshCells()</code> only needs to run when a new variable is added or removed. How can we achieve this?</p> 462<ul class="list_spread"> 463<li> 464<p>A naive way to do that would be to rewrite <code class="language-text">useEffect</code> dependencies like this:</p> 465<div class="gatsby-highlight" data-language="tsx"><pre class="language-tsx"><code class="language-tsx"><span class="token function">useEffect</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 466 <span class="token comment">// ...</span> 467<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>gridApi<span class="token punctuation">,</span> autocompleteVariables<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div> 468<p>â</p> 469<div class="gatsby-highlight" data-language="tsx"><pre class="language-tsx"><code class="language-tsx"><span class="token function">useEffect</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 470 <span class="token comment">// ...</span> 471<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>gridApi<span class="token punctuation">,</span> autocompleteVariables<span class="token punctuation">.</span>length<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div> 472<p>This will work in most cases. However, if we add one variable and remove another one simultaneously, the workaround wonât run.</p> 473</li> 474<li> 475<p>A <a href="https://www.youtube.com/watch?v=RW9TVhmxu6Q">proper way</a> to do that would be to move <code class="language-text">gridApi.refreshCells()</code> to the code that adds or removes a variable â e.g., to a Redux saga that handles the corresponding action.</p> 476<p>However, this isnât a simple change. The logic that uses <code class="language-text">gridApi</code> is concentrated in a single component. Exposing <code class="language-text">gridApi</code> to the Redux code would require us to break/change several abstractions. Weâre working on this, but this will take time.</p> 477</li> 478<li> 479<p>Instead, while Causal is working on a proper solution, why donât we hack a bit more? ð </p> 480<div class="gatsby-highlight" data-language="tsx"><pre class="language-tsx"><code class="language-tsx"><span class="token function">useEffect</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 481 <span class="token comment">// ...</span> 482<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>gridApi<span class="token punctuation">,</span> autocompleteVariables<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div> 483<p>â</p> 484<div class="gatsby-highlight" data-language="tsx"><pre class="language-tsx"><code class="language-tsx"><span class="token function">useEffect</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 485 <span class="token comment">// ...</span> 486<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>gridApi<span class="token punctuation">,</span> Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span>autocompleteVariables<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">
486sort</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">','</span><span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div> 487<p>With this change, <code class="language-text">useEffect</code> will depend only on concrete variable IDs inside <code class="language-text">autocompleteVariables</code>. Unless any variable ids get added or removed, the <code class="language-text">useEffect</code> shouldnât run anymore. (This assumes none of the variable ids include a <code class="language-text">,</code> character, which is true in Causalâs case.)</p> 488</li> 489</ul> 490<p>Terrible? Yes. Temporary, contained, and <a href="https://programmingisterrible.com/post/139222674273/how-to-write-disposable-code-in-large-systems">easy to delete</a>, bearing the minimal technical debt? Also yes. Solves the real issue? Absolutely yes. The real world is about tradeoffs, and sometimes you have to write less-than-optimal code if it makes your usersâ life better.</p> 491<p>Just like that, we save another 5-10% of the JavaScript execution time.</p> 492<h1 id="deep-areequal" style="position:relative;"><a href="#deep-areequal" aria-label="deep areequal permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Deep <code class="language-text">areEqual</code></h1> 493<p>There are a few bits in the performance trace that look like this:</p> 494<figure class="media-container"><picture><img src="/static/96bd6789e6df1d75c54d6b87c16eb605/e7448/image23.png" srcset="/static/96bd6789e6df1d75c54d6b87c16eb605/0608e/image23.png 225w, 495/static/96bd6789e6df1d75c54d6b87c16eb605/31401/image23.png 450w, 496/static/96bd6789e6df1d75c54d6b87c16eb605/e7448/image23.png 900w, 497/static/96bd6789e6df1d75c54d6b87c16eb605/9ac8b/image23.png 1350w, 498/static/96bd6789e6df1d75c54d6b87c16eb605/28bdc/image23.png 1800w, 499/static/96bd6789e6df1d75c54d6b87c16eb605/7f509/image23.png 2648w" alt="" width="900" height="1098"></picture></figure> 500<p>What happens here is we have a function called <code class="language-text">areEqual</code>. This function calls a function called <code class="language-text">areEquivalent</code> â and then <code class="language-text">areEquivalent</code> calls itself multiple times, over and over again. This is a deep equality comparison, and on a 2020 MacBook Pro, it takes ~90 ms.</p> 501<p>The <code class="language-text">areEqual</code> function <a href="https://github.com/ag-grid/ag-grid/blob/fa0f9a4befe0486ebfb232a257eba2e6fb1e7b23/community-modules/react/src/shared/changeDetectionService.ts#L24-L26">comes from AG Grid</a>. Hereâs how itâs called:</p> 502<ol class="list_spread"> 503<li> 504<p>React <a href="https://github.com/ag-grid/ag-grid/blob/3ba94de74e5e3a7adc3aaa1a476e867f8f550fac/community-modules/react/src/reactUi/agGridReactUi.tsx#L133-L135">calls <code class="language-text">componentDidUpdate()</code></a> in AG Grid whenever the component rerenders:</p> 505<div class="gatsby-highlight" data-language="tsx"><pre class="language-tsx"><code class="language-tsx"><span class="token keyword">class</span> <span class="token class-name">AgGridReactUi</span> <span class="token punctuation">{</span> 506 <span class="token function">componentDidUpdate</span><span class="token punctuation">(</span>prevProps<span class="token punctuation">)</span> <span class="token punctuation">{</span> 507 <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">processPropsChanges</span><span class="token punctuation">(</span>prevProps<span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>props<span class="token punctuation">)</span><span class="token punctuation">;</span> 508 <span class="token punctuation">}</span> 509<span class="token punctuation">}</span></code></pre></div> 510</li> 511<li> 512<p><code class="language-text">componentDidUpdate()</code> invokes <a href="https://github.com/ag-grid/ag-grid/blob/3ba94de74e5e3a7adc3aaa1a476e867f8f550fac/community-modules/react/src/reactUi/agGridReactUi.tsx#L137-L144"><code class="language-text">processPropChanges()</code></a>:</p> 513<div class="gatsby-highlight" data-language="tsx"><pre class="language-tsx"><code class="language-tsx"><span class="token keyword">public</span> <span class="token function">processPropsChanges</span><span class="token punctuation">(</span>prevProps<span class="token operator">:</span> <span class="token builtin">any</span><span class="token punctuation">,</span> nextProps<span class="token operator">:</span> <span class="token builtin">any</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> 514 <span class="token keyword">const</span> changes <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">;</span> 515 516 <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">extractGridPropertyChanges</span><span class="token punctuation">(</span>prevProps<span class="token punctuation">,</span> nextProps<span class="token punctuation">,</span> changes<span class="token punctuation">)</span><span class="token punctuation">;</span> 517 <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">extractDeclarativeColDefChanges</span><span class="token punctuation">(</span>nextProps<span class="token punctuation">,</span> changes<span class="token punctuation">)</span><span class="token punctuation">;</span> 518 519 <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">processChanges</span><span class="token punctuation">(</span>changes<span class="token punctuation">)</span><span class="token punctuation">;</span> 520<span class="token punctuation">}</span></code></pre></div> 521</li> 522<li> 523<p>Among other things, <code class="language-text">processPropChanges()</code> calls a function called <a href="https://github.com/ag-grid/ag-grid/blob/3ba94de74e5e3a7adc3aaa1a476e867f8f550fac/community-modules/react/src/reactUi/agGridReactUi.tsx#L181-L199"><code class="language-text">extractGridPropertyChanges()</code></a></p> 524</li> 525<li> 526<p><code class="language-text">extractGridPropertyChanges()</code> then performs a deep comparison on every prop passed into <code class="language-text">AgGridReactUi</code>:</p> 527<div class="gatsby-highlight" data-language="tsx"><pre class="language-tsx"><code class="language-tsx"><span class="token comment">// The code is simplified</span> 528<span class="token keyword">private</span> <span class="token function">extractGridPropertyChanges</span><span class="token punctuation">(</span>prevProps<span class="token operator">:</span> <span class="token builtin">any</span><span class="token punctuation">,</span> nextProps<span class="token operator">:</span> <span class="token builtin">any</span><span class="token punctuation">,</span> changes<span class="token operator">:</span> <span class="token builtin">any</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> 529 Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span>nextProps<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span>propKey <span class="token operator">=></span> <span class="token punctuation">{</span>
530 <span class="token keyword">if</span> <span class="token punctuation">(</span>_<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span>ComponentUtil<span class="token punctuation">.</span><span class="token constant">ALL_PROPERTIES</span><span class="token punctuation">,</span> propKey<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> 531 <span class="token keyword">const</span> changeDetectionStrategy <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>changeDetectionService<span class="token punctuation">.</span><span class="token function">getStrategy</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getStrategyTypeForProp</span><span class="token punctuation">(</span>propKey<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 532 533 <span class="token comment">// â Here</span> 534 <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>changeDetectionStrategy<span class="token punctuation">.</span><span class="token function">areEqual</span><span class="token punctuation">(</span>prevProps<span class="token punctuation">[</span>propKey<span class="token punctuation">]</span><span class="token punctuation">,</span> nextProps<span class="token punctuation">[</span>propKey<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> 535 <span class="token comment">// ...</span> 536 <span class="token punctuation">}</span> 537 <span class="token punctuation">}</span> 538 <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div> 539<p>If some of these props are huge and change significantly, the deep comparison will take a lot of time. Unfortunately, this is precisely whatâs happening here.</p> 540</li> 541</ol> 542<p>With a bit of debugging and <a href="https://developer.mozilla.org/en-US/docs/Web/API/Console/time"><code class="language-text">console.time()</code></a>, we find out that the expensive prop is <code class="language-text">context</code>. <code class="language-text">context</code> is an object holding a bunch of variables that we need to pass down to grid components. The object changes for good:</p> 543<div class="gatsby-highlight" data-language="tsx"><pre class="language-tsx"><code class="language-tsx"><span class="token keyword">const</span> context<span class="token operator">:</span> GridContext <span class="token operator">=</span> <span class="token function">useMemo</span><span class="token punctuation">(</span> 544 <span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> GridContext <span class="token operator">=></span> <span class="token punctuation">(</span><span class="token punctuation">{</span> 545 allDimensions<span class="token punctuation">,</span> 546 autocompleteVariables<span class="token punctuation">,</span> 547 <span class="token comment">// â A few values in the model change (as they should).</span> 548 <span class="token comment">// This rebuilds the `editorModel` â and the `context` object itself</span> 549 editorModel<span class="token punctuation">,</span> 550 filteredDimensions<span class="token punctuation">,</span> 551 isReadOnly<span class="token punctuation">,</span> 552 modelId<span class="token punctuation">,</span> 553 scenarioId<span class="token operator">:</span> activeScenario<span class="token punctuation">.</span>id<span class="token punctuation">,</span> 554 showFormulas<span class="token punctuation">,</span> 555 <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> 556 <span class="token punctuation">[</span> 557 activeScenario<span class="token punctuation">.</span>id<span class="token punctuation">,</span> 558 allDimensions<span class="token punctuation">,</span> 559 autocompleteVariables<span class="token punctuation">,</span> 560 editorModel<span class="token punctuation">,</span> 561 filteredDimensions<span class="token punctuation">,</span> 562 isReadOnly<span class="token punctuation">,</span> 563 modelId<span class="token punctuation">,</span> 564 showFormulas<span class="token punctuation">,</span> 565 <span class="token punctuation">]</span><span class="token punctuation">,</span> 566<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div> 567<p>However, using a deep comparison on such a massive object is bad and unnecessary. The object is memoized, so we can just <code class="language-text">===</code> it to figure out whether it changed. But how do we do that?</p> 568<p>AG Grid supports <a href="https://github.com/ag-grid/ag-grid/blob/fa0f9a4befe0486ebfb232a257eba2e6fb1e7b23/community-modules/react/src/shared/changeDetectionService.ts#L1-L5">several comparison strategies for props</a>. One of them implements a <code class="language-text">===</code> comparison:</p> 569<div class="gatsby-highlight" data-language="tsx"><pre class="language-tsx"><code class="language-tsx"><span class="token keyword">export</span> <span class="token keyword">enum</span> ChangeDetectionStrategyType <span class="token punctuation">{</span> 570 IdentityCheck <span class="token operator">=</span> <span class="token string">'IdentityCheck'</span><span class="token punctuation">,</span> <span class="token comment">// Uses === to compare objects</span> 571 DeepValueCheck <span class="token operator">=</span> <span class="token string">'DeepValueCheck'</span><span class="token punctuation">,</span> <span class="token comment">// Uses deep comparison to compare objects</span> 572 NoCheck <span class="token operator">=</span> <span class="token string">'NoCheck'</span><span class="token punctuation">,</span> <span class="token comment">// Always considers objects different</span> 573<span class="token punctuation">}</span></code></pre></div> 574<p>However, based on the source code, we can specify a custom strategy <a href="https://github.com/ag-grid/ag-grid/blob/fa0f9a4befe0486ebfb232a257eba2e6fb1e7b23/community-modules/react/src/reactUi/agGridReactUi.tsx#L231-L243">only for the <code class="language-text">rowData</code> prop</a>:</p> 575<div class="gatsby-highlight" data-language="tsx"><pre class="language-tsx"><code class="language-tsx"><span class="token comment">// This function chooses how to compare any given prop</span> 576<span class="token function">getStrategyTypeForProp</span><span class="token punctuation">(</span>propKey<span class="token punctuation">)</span> <span class="token punctuation">{</span> 577 <span class="token keyword">if</span> <span class="token punctuation">(</span>propKey <span class="token operator">===</span> <span class="token string">'rowData'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> 578 <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>props<span class="token punctuation">.</span>rowDataChangeDetectionStrategy<span class="token punctuation">)</span> <span class="token punctuation">{</span>
579 <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>props<span class="token punctuation">.</span>rowDataChangeDetectionStrategy<span class="token punctuation">;</span> 580 <span class="token punctuation">}</span> 581 <span class="token comment">// ...</span> 582 <span class="token punctuation">}</span> 583 584 <span class="token keyword">return</span> ChangeDetectionStrategyType<span class="token punctuation">.</span>DeepValueCheck<span class="token punctuation">;</span> 585<span class="token punctuation">}</span></code></pre></div> 586<p>But nothing is preventing us from patching AG Grid again, right? Using <a href="https://yarnpkg.com/cli/patch/">yarn patch</a>, <a href="#ag-grid-fixing-an-extra-render">like we did above</a>, letâs add a few lines into the <code class="language-text">getStrategyTypeForProp()</code> function:</p> 587<div class="gatsby-highlight" data-language="tsx"><pre class="language-tsx"><code class="language-tsx"><span class="token function">getStrategyTypeForProp</span><span class="token punctuation">(</span>propKey<span class="token punctuation">)</span> <span class="token punctuation">{</span> 588 <span class="token comment">// NEW</span> 589 <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>props<span class="token punctuation">.</span>changeDetectionStrategies <span class="token operator">&&</span> propKey <span class="token keyword">in</span> <span class="token keyword">this</span><span class="token punctuation">.</span>props<span class="token punctuation">.</span>changeDetectionStrategies<span class="token punctuation">)</span> <span class="token punctuation">{</span> 590 <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>props<span class="token punctuation">.</span>changeDetectionStrategies<span class="token punctuation">[</span>propKey<span class="token punctuation">]</span><span class="token punctuation">;</span> 591 <span class="token punctuation">}</span> 592 <span class="token comment">// END OF NEW</span> 593 594 <span class="token keyword">if</span> <span class="token punctuation">(</span>propKey <span class="token operator">===</span> <span class="token string">'rowData'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> 595 <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>props<span class="token punctuation">.</span>rowDataChangeDetectionStrategy<span class="token punctuation">)</span> <span class="token punctuation">{</span> 596 <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>props<span class="token punctuation">.</span>rowDataChangeDetectionStrategy<span class="token punctuation">;</span> 597 <span class="token punctuation">}</span> 598 <span class="token comment">// ...</span> 599 <span class="token punctuation">}</span> 600 601 <span class="token comment">// all other cases will default to DeepValueCheck</span> 602 <span class="token keyword">return</span> ChangeDetectionStrategyType<span class="token punctuation">.</span>DeepValueCheck<span class="token punctuation">;</span> 603<span class="token punctuation">}</span></code></pre></div> 604<p>With this change, we can specify a custom comparison strategy for the <code class="language-text">context</code> prop:</p> 605<div class="gatsby-highlight" data-language="tsx"><pre class="language-tsx"><code class="language-tsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> ChangeDetectionStrategyType <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"@ag-grid-community/react/lib/shared/changeDetectionService"</span><span class="token punctuation">;</span> 606 607<span class="token comment">// ...</span> 608 609<span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">AgGridReact</span></span> 610 <span class="token attr-name">changeDetectionStrategies</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token punctuation">{</span> 611 context<span class="token operator">:</span> ChangeDetectionStrategyType<span class="token punctuation">.</span>IdentityCheck 612 <span class="token punctuation">}</span><span class="token punctuation">}</span></span> 613 <span class="token comment">
613// ...</span> 614<span class="token punctuation">></span></span></code></pre></div> 615<p>And, just like that, we save another 3-5% of the JavaScript cost.</p> 616<h1 id="whats-still-in-the-works" style="position:relative;"><a href="#whats-still-in-the-works" aria-label="whats still in the works permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Whatâs Still In The Works</h1> 617<h2 id="more-granular-updates" style="position:relative;"><a href="#more-granular-updates" aria-label="more granular updates permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>More Granular Updates</h2> 618<p>You mightâve noticed that for one update of a category value, we rerender the data grid four times:</p> 619<figure class="media-container"><picture><img src="/static/c508070c56091d1a3981d349ecd2893f/e7448/image24.png" srcset="/static/c508070c56091d1a3981d349ecd2893f/0608e/image24.png 225w, 620/static/c508070c56091d1a3981d349ecd2893f/31401/image24.png 450w, 621/static/c508070c56091d1a3981d349ecd2893f/e7448/image24.png 900w, 622/static/c508070c56091d1a3981d349ecd2893f/9ac8b/image24.png 1350w, 623/static/c508070c56091d1a3981d349ecd2893f/28bdc/image24.png 1800w, 624/static/c508070c56091d1a3981d349ecd2893f/7f509/image24.png 2648w" alt="" width="900" height="589"></picture></figure> 625<p>Four renders are three renders too many. The UI should update only once if a user makes a single change. However, solving this is challenging.</p> 626<p>Hereâs a <code class="language-text">useEffect</code> that rerenders the data grid:</p> 627<div class="gatsby-highlight" data-language="tsx"><pre class="language-tsx"><code class="language-tsx"><span class="token function">useEffect</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 628 <span class="token keyword">const</span> pathsToRefresh<span class="token operator">:</span> <span class="token punctuation">{</span> route<span class="token operator">:</span> RowId<span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">}</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span> 629 <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> <span class="token punctuation">[</span>pathString<span class="token punctuation">,</span> oldRows<span class="token punctuation">]</span> <span class="token keyword">of</span> rowCache<span class="token punctuation">.</span>current<span class="token punctuation">.</span><span class="token function">entries</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> 630 <span class="token comment">// Fill the pathsToRefresh object (code omitted)</span> 631 <span class="token punctuation">}</span> 632 633 <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> refreshParams <span class="token keyword">of</span> pathsToRefresh<span class="token punctuation">)</span> <span class="token punctuation">{</span> 634 gridApi<span class="token operator">?.</span><span class="token function">refreshServerSide</span><span class="token punctuation">(</span>refreshParams<span class="token punctuation">)</span><span class="token punctuation">;</span> 635 <span class="token punctuation">}</span> 636<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span> 637 editorModel<span class="token punctuation">,</span> 638 gridApi<span class="token punctuation">,</span> 639 variableDimensionsLookup<span class="token punctuation">,</span> 640 activeScenario<span class="token punctuation">,</span> 641 variableGetterRef<span class="token punctuation">,</span> 642<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div> 643<p>To figure out whatâs causing this <code class="language-text">useEffect</code> to re-run, letâs use <a href="https://usehooks.com/useWhyDidYouUpdate/"><code class="language-text">useWhyDidYouUpdate</code></a>:</p> 644<div class="gatsby-highlight" data-language="tsx"><pre class="language-tsx"><code class="language-tsx"><span class="token function">useEffect</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 645 <span class="token comment">
645// ...</span> 646<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span> 647 editorModel<span class="token punctuation">,</span> 648 gridApi<span class="token punctuation">,</span> 649 variableDimensionsLookup<span class="token punctuation">,</span> 650 activeScenario<span class="token punctuation">,</span> 651 variableGetterRef<span class="token punctuation">,</span> 652<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 653 654<span class="token function">useWhyDidYouUpdate</span><span class="token punctuation">(</span><span class="token string">'datasource'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> 655 editorModel<span class="token punctuation">,</span> 656 gridApi<span class="token punctuation">,</span> 657 variableDimensionsLookup<span class="token punctuation">,</span> 658 activeScenario<span class="token punctuation">,</span> 659 variableGetterRef<span class="token punctuation">,</span> 660<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div> 661<p><code class="language-text">useWhyDidYouUpdate</code> will print this:</p> 662<figure class="media-container"><picture><img src="/static/0afad00fe191d80875b0bc77cf37bf69/e7448/image25.png" srcset="/static/0afad00fe191d80875b0bc77cf37bf69/0608e/image25.png 225w, 663/static/0afad00fe191d80875b0bc77cf37bf69/31401/image25.png 450w, 664/static/0afad00fe191d80875b0bc77cf37bf69/e7448/image25.png 900w, 665/static/0afad00fe191d80875b0bc77cf37bf69/9ac8b/image25.png 1350w, 666/static/0afad00fe191d80875b0bc77cf37bf69/28bdc/image25.png 1800w, 667/static/0afad00fe191d80875b0bc77cf37bf69/4b883/image25.png 2380w" alt="" width="900" height="625"></picture></figure> 668<p>This tells us <code class="language-text">useEffect</code> re-runs because <code class="language-text">editorModel</code> and <code class="language-text">variableDimensionsLookup</code> objects change. But how? With <a href="https://gist.github.com/iamakulov/5f1e74cdcffc8a0bb3d2278dd54d8328">a little custom <code class="language-text">deepCompare</code> function</a>, we can figure this out:</p> 669<figure class="media-container"><picture><img src="/static/5b59d302698b774c086563dc507a5da8/e7448/image26.png" srcset="/static/5b59d302698b774c086563dc507a5da8/0608e/image26.png 225w, 670/static/5b59d302698b774c086563dc507a5da8/31401/image26.png 450w, 671/static/5b59d302698b774c086563dc507a5da8/e7448/image26.png 900w, 672/static/5b59d302698b774c086563dc507a5da8/9ac8b/image26.png 1350w, 673/static/5b59d302698b774c086563dc507a5da8/28bdc/image26.png 1800w, 674/static/5b59d302698b774c086563dc507a5da8/5e75c/image26.png 2154w" alt="" width="900" height="665"></picture></figure> 675<p>This is how <code class="language-text">editorModel</code> changes if you update a single category value from <code class="language-text">69120</code> to <code class="language-text">5</code>. As you see, a single change causes four consecutive updates. <code class="language-text">variableDimensionsLookup</code> changes similarly (not shown)</p> 676<p>One category update causes four <code class="language-text">editorModel</code> updates. Some of these updates are caused by suboptimal Redux sagas (which weâre fixing). Others (like update 4, which rebuilds the model but doesnât change anything) may be fixed by adding extra <a href="https://github.com/reduxjs/reselect">memoized selectors</a> or comparison checks.</p> 677<p>But thereâs also a deeper, fundamental issue that is harder to fix. With React and Redux, the code we write by default <em>is not performant</em>. React and Redux donât help us to fall <a href="https://blog.codinghorror.com/falling-into-the-pit-of-success/">into a pit of success</a>.</p> 678<p>To make the code fast, we need to remember to memoize most computations â both in components (with <code class="language-text">useMemo</code> and <code class="language-text">useCallback</code>) and in Redux selectors (with <code class="language-text">reselect</code>). If we donât do that, some components will rerender unnecessarily. Thatâs cheap in smaller apps but scales really, really poorly as your app grows.</p> 679<p>And some of these computations are not really memoizable:</p> 680<div class="gatsby-highlight" data-language="tsx"><pre class="language-tsx"><code class="language-tsx"><span class="token keyword">const</span> variableValues <span class="token operator">=</span> <span class="token function">useMemo</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 681 <span class="token keyword">return</span> Object<span class="token punctuation">.</span><span class="token function">values</span><span class="token punctuation">(</span>editorModel<span class="token punctuation">.</span>variables<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span>variable<span class="token punctuation">)</span> <span class="token operator">=></span> variable<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">;</span> 682<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>editorModel<span class="token punctuation">.</span>variables<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 683<span class="token comment">// â Recalculates on every `editorModel.variables` change.</span> 684<span class="token comment">// But how do you recalculate it only when `editorModel.variables[...].value`s change?</span> 685<span class="token comment">// ("Deep comparison" is a possible answer, but it's very expensive with large objects.)</span></code></pre></div> 686<p>This also affects the <code class="language-text">useEffect</code> we saw above:</p> 687<div class="gatsby-highlight" data-language="tsx"><pre class="language-tsx"><code class="language-tsx"><span class="token function">useEffect</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 688 <span class="token comment">// Re-render the data grid</span> 689<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>editorModel <span class="token comment">/* ... */</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 690<span class="token comment">// â Re-runs on every `editorModel` change</span> 691<span class="token comment">// But how do you express "re-run it only when `editorModel.variables[...].value`s change"?</span></code></pre></div> 692<p>Weâve identified this improvement with Causal, and Causal is currently working on solving these extra renders (e.g., by <a href="https://www.youtube.com/watch?v=RW9TVhmxu6Q">moving logic away from <code class="language-text">useEffect</code>s</a>). In our tests, this should cut another 10-30% off the JavaScript cost. But this will take some time.</p> 693<blockquote class="note"><p><strong>React Forget.</strong> To be fair, React is also working on <a href="https://reactjs.org/blog/2022/06/15/react-labs-what-we-have-been-working-on-june-2022.html#react-compiler">an auto-memoizing compiler</a> which should reduce recalculations.</p></blockquote> 694<h2 id="useselector-vs-usestore" style="position:relative;"><a href="#useselector-vs-usestore" aria-label="useselector vs usestore permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a><code class="language-text">useSelector</code> vs <code class="language-text">useStore</code></h2> 695<p>If you have some data in a Redux store, and you want to access that data in an <code class="language-text">onChange</code> callback, how would you do that?</p> 696<p>Hereâs the most straightforward way:</p> 697<div class="gatsby-highlight" data-language="php"><pre class="language-php"><code class="language-php"><span class="token keyword">const</span> CellWrapper <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 698 <span class="token keyword">const</span> <span class="token constant">
698editorModel</span> <span class="token operator">=</span> <span class="token function">useSelector</span><span class="token punctuation">(</span>state <span class="token operator">=></span> state<span class="token operator">.</span>editorModel<span class="token punctuation">)</span> 699 <span class="token keyword">const</span> <span class="token constant">onChange</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 700 <span class="token comment">// Do something with editorModel</span> 701 <span class="token punctuation">}</span> 702 703 <span class="token keyword">return</span> <span class="token operator"><</span>Cell onChange<span class="token operator">=</span><span class="token punctuation">{</span>onChange<span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span> 704<span class="token punctuation">}</span></code></pre></div> 705<p>If <code class="language-text"><Cell></code> is expensive to rerender, and you want to avoid rerendering it unnecessarily, you might wrap <code class="language-text">onChange</code> with <code class="language-text">useCallback</code>:</p> 706<div class="gatsby-highlight" data-language="php"><pre class="language-php"><code class="language-php"><span class="token keyword">const</span> CellWrapper <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 707 <span class="token keyword">const</span> <span class="token constant">editorModel</span> <span class="token operator">=</span> <span class="token function">useSelector</span><span class="token punctuation">(</span>
707state <span class="token operator">=></span> state<span class="token operator">.</span>editorModel<span class="token punctuation">)</span> 708 <span class="token keyword">const</span> <span class="token constant">onChange</span> <span class="token operator">=</span> <span class="token function">useCallback</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 709 <span class="token comment">// Do something with editorModel</span> 710 <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>editorModel<span class="token punctuation">]</span><span class="token punctuation">)</span> 711 712 <span class="token keyword">return</span> <span class="token operator"><</span>Cell onChange<span class="token operator">=</span><span class="token punctuation">{</span>onChange<span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span> 713<span class="token punctuation">}</span></code></pre></div> 714<p>However, what will happen if <code class="language-text">editorModel</code> changes very often? Right â <code class="language-text">useCallback</code> will regenerate <code class="language-text">onChange</code> whenever <code class="language-text">editorModel</code> changes, and <code class="language-text"><Cell></code> will rerender every time.</p> 715<p>Hereâs an alternative approach that doesnât have this issue:</p> 716<div class="gatsby-highlight" data-language="php"><pre class="language-php"><code class="language-php"><span class="token keyword">const</span> CellWrapper <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 717 <span class="token keyword">const</span> <span class="token constant">store</span> <span class="token operator">=</span> <span class="token function">useStore</span><span class="token punctuation">(</span><span class="token punctuation">)</span> 718 <span class="token keyword">const</span> <span class="token constant">onChange</span> <span class="token operator">=</span> <span class="token function">useCallback</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 719 <span class="token keyword">const</span> <span class="token constant">editorModel</span> <span class="token operator">=</span> store<span class="token operator">.</span><span class="token function">getState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">.</span>editorModel 720 <span class="token comment">// Do something with editorModel</span> 721 <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>store<span class="token punctuation">]</span><span class="token punctuation">)</span> 722 723 <span class="token keyword">return</span> <span class="token operator"><</span>Cell onChange<span class="token operator">=</span><span class="token punctuation">{</span>onChange<span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span> 724<span class="token punctuation">}</span></code></pre></div> 725<p>This approach relies on Reduxâs <a href="https://react-redux.js.org/api/hooks#usestore"><code class="language-text">useStore()</code></a> hook.</p> 726<ul class="list_spread"> 727<li> 728<p>Unlike <code class="language-text">useSelector()</code>, <code class="language-text">useStore()</code> returns the full store object.</p> 729</li> 730<li> 731<p>Also, unlike <code class="language-text">useSelector()</code>, <code class="language-text">useStore()</code> canât trigger a component render. But we donât need to, either! The component output doesnât rely on the <code class="language-text">
731editorModel</code> state. Only the <code class="language-text">onChange</code> callback needs it â and we can safely delay the <code class="language-text">editorModel</code> read until then.</p> 732</li> 733</ul> 734<p>Causal has a bunch of components using <code class="language-text">useCallback</code> and <code class="language-text">useSelector</code> like above. They would benefit from this optimization, so Causal is gradually implementing it. We didnât see any immediate improvements in the interaction we were optimizing, but we expect this to reduce rerenders in a few other places.</p> 735<blockquote class="note"><p><strong><code class="language-text">useEvent</code>.</strong> In the future, wrapping the callback with <a href="https://github.com/reactjs/rfcs/pull/220"><code class="language-text">useEvent</code></a> instead of <code class="language-text">useCallback</code> might help solve this issue too.</p></blockquote> 736<h1 id="what-didnt-work" style="position:relative;"><a href="#what-didnt-work" aria-label="what didnt work permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>What Didnât Work</h1> 737<h2 id="web-workers" style="position:relative;"><a href="#web-workers" aria-label="web workers permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Web Workers</h2> 738<p>Hereâs another bit of the performance trace:</p> 739<figure class="media-container"><picture><img src="/static/6464d4ee1cce35c6e9b41ac0d8d17400/e7448/image27.png" srcset="/static/6464d4ee1cce35c6e9b41ac0d8d17400/0608e/image27.png 225w, 740/static/6464d4ee1cce35c6e9b41ac0d8d17400/31401/image27.png 450w, 741/static/6464d4ee1cce35c6e9b41ac0d8d17400/e7448/image27.png 900w, 742/static/6464d4ee1cce35c6e9b41ac0d8d17400/9ac8b/image27.png 1350w, 743/static/6464d4ee1cce35c6e9b41ac0d8d17400/28bdc/image27.png 1800w, 744/static/6464d4ee1cce35c6e9b41ac0d8d17400/7f509/image27.png 2648w" alt="" width="900" height="882"></picture></figure> 745<p>In this part of the trace, we receive a new binary-encoded model from the server and parse it using <
745a href="https://github.com/protobufjs/protobuf.js/">protobuf</a>. This is a self-contained operation (you call a single function, and it returns 400-800 ms later), and it doesnât need to access DOM. This makes it a perfect candidate for a Web Worker.</p> 746<blockquote class="note"><p><strong>Web What?</strong> <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers">Web Workers</a> are a way to run some expensive JavaScript in a separate thread. This allows to keep the page responsive while that JavaScript is running.</p></blockquote> 747<p>The easiest way to move a function into a web worker is to wrap it with <a href="https://github.com/GoogleChromeLabs/comlink">comlink</a>:</p> 748<div class="gatsby-highlight" data-language="tsx"><pre class="language-tsx"><code class="language-tsx"><span class="token comment">// index.ts</span> 749 750<span class="token comment">// Take this code:</span> 751<span class="token keyword">import</span> <span class="token punctuation">{</span> decodeResponse <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'causal-common/eval/proto/generated/eval_pb'</span><span class="token punctuation">;</span> 752<span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token function">decodeResponse</span><span class="token punctuation">(</span>rawResponse<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div> 753<p>â</p> 754<div class="gatsby-highlight" data-language="typescript"><pre class="language-typescript"><code class="language-typescript"><span class="token comment">// worker.ts</span> 755 756<span class="token comment">// Move it into a new file and wrap it with `Comlink.expose`:</span> 757<span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> Comlink <span class="token keyword">from</span> <span class="token string">'comlink'</span><span class="token punctuation">;</span> 758<span class="token keyword">import</span> <span class="token punctuation">{</span> decodeResponse <span class="token keyword">as</span> decodeResponseImported <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'causal-common/eval/proto/generated/eval_pb'</span><span class="token punctuation">;</span> 759 760Comlink<span class="token punctuation">.</span><span class="token function">expose</span><span class="token punctuation">(</span><span class="token punctuation">{</span> 761 <span class="token function-variable function">decodeResponse</span><span class="token operator">:</span> <span class="token punctuation">(</span>rawResponse<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 762 <span class="token keyword">return</span> <span class="token function">decodeResponseImported</span><span class="token punctuation">(</span>rawResponse<span class="token punctuation">)</span><span class="token punctuation">;</span> 763 <span class="token punctuation">}</span><span class="token punctuation">,</span> 764<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div> 765<div class="sidenote"><div class="sidenote__body"><div class="gatsby-highlight" data-language="tsx"><pre class="language-tsx"><code class="language-tsx"><span class="token comment">// index.ts</span> 766 767<span class="token comment">
767// In the original file, create a worker...</span> 768<span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> Comlink <span class="token keyword">from</span> <span class="token string">'comlink'</span><span class="token punctuation">;</span> 769<span class="token keyword">const</span> worker <span class="token operator">=</span> Comlink<span class="token punctuation">.</span><span class="token function">wrap</span><span class="token punctuation">(</span> 770 <span class="token keyword">new</span> <span class="token class-name">Worker</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name"><span class="token constant">URL</span></span><span class="token punctuation">(</span><span class="token string">'./worker.ts'</span><span class="token punctuation">,</span> <span class="token keyword">import</span><span class="token punctuation">.</span>meta<span class="token punctuation">.</span>url<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span> 771<span class="token punctuation">)</span><span class="token punctuation">;</span> 772 773<span class="token comment">// And just prefix the original function call with `await worker.`</span> 774<span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> worker<span class="token punctuation">.</span><span class="token function">decodeResponse</span><span class="token punctuation">(</span>rawResponse<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div></div><blockquote class="sidenote__heading"><strong class="sidenote__remark">Sidenote:</strong> The <code class="language-text">new Worker()</code> syntax works <a href="https://webpack.js.org/guides/web-workers/">with webpack 5</a>, <a href="https://vitejs.dev/guide/features.html#web-workers">Vite</a>, and <a href="https://github.com/evanw/esbuild/issues/312#issuecomment-1232418832">will be supported in esbuild soon</a>.</blockquote></div> 775<p>
775If we do that and record a new trace, weâll discover that parsing was successfully moved to the worker thread:</p> 776<figure class="media-container"><picture><img src="/static/997a0f1684aefd37332ef28beaea8549/e7448/image28.png" srcset="/static/997a0f1684aefd37332ef28beaea8549/0608e/image28.png 225w, 777/static/997a0f1684aefd37332ef28beaea8549/31401/image28.png 450w, 778/static/997a0f1684aefd37332ef28beaea8549/e7448/image28.png 900w, 779/static/997a0f1684aefd37332ef28beaea8549/9ac8b/image28.png 1350w, 780/static/997a0f1684aefd37332ef28beaea8549/28bdc/image28.png 1800w, 781/static/997a0f1684aefd37332ef28beaea8549/7b4c8/image28.png 2438w" alt="" width="900" height="611"></picture></figure> 782<p>However, weirdly, the overall JS cost will increase. Thatâs because the main thread and the worker thread will add two new flat 400-6000ms long chunks of JavaScript:</p> 783<figure class="media-container"><picture><img src="/static/1d8f3988280f8432cf4216fdb5b36dd3/e7448/image29.png" srcset="/static/1d8f3988280f8432cf4216fdb5b36dd3/0608e/image29.png 225w, 784/static/1d8f3988280f8432cf4216fdb5b36dd3/31401/image29.png 450w, 785/static/1d8f3988280f8432cf4216fdb5b36dd3/e7448/image29.png 900w, 786/static/1d8f3988280f8432cf4216fdb5b36dd3/9ac8b/image29.png 1350w, 787/static/1d8f3988280f8432cf4216fdb5b36dd3/28bdc/image29.png 1800w, 788/static/1d8f3988280f8432cf4216fdb5b36dd3/7b4c8/image29.png 2438w" alt="" width="900" height="611"></picture><figcaption class="media-container__caption">Worker thread</figcaption></figure> 789<figure class="media-container"><picture><img src="/static/07c50f26bfd07cae0f765102e112849d/e7448/image30.png" srcset="/static/07c50f26bfd07cae0f765102e112849d/0608e/image30.png 225w, 790/static/07c50f26bfd07cae0f765102e112849d/31401/image30.png 450w, 791/static/07c50f26bfd07cae0f765102e112849d/e7448/image30.png 900w, 792/static/07c50f26bfd07cae0f765102e112849d/9ac8b/image30.png 1350w, 793/static/07c50f26bfd07cae0f765102e112849d/28bdc/image30.png 1800w, 794/static/07c50f26bfd07cae0f765102e112849d/7b4c8/image30.png 2438w" alt="" width="900" height="611"></picture><figcaption class="media-container__caption">Main thread</figcaption></figure> 795<p>It turns out that moving stuff to a web worker isnât free. Whenever you pass data from and to a web worker, the browser <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm">has to serialize and deserialize it</a>. Typically, this is cheap; however, for large objects, this may take some time. For Causal, because the model was so large, this took longer than actually parsing the model!</p> 796<p>Unfortunately, this optimization didnât work for Causal. Instead, as an experiment, Causal has started working on selective data loading (fetching only visible rows instead of the whole model). In our tests, with selective data loading, the parsing costs go down from 500-1500 ms to 1-5 ms:</p> 797<figure class="media-container"><picture><img src="/static/06e8b844cd161cf27413aaf180c0387d/e7448/image31.png" srcset="/static/06e8b844cd161cf27413aaf180c0387d/0608e/image31.png 225w, 798/static/06e8b844cd161cf27413aaf180c0387d/31401/image31.png 450w, 799/static/06e8b844cd161cf27413aaf180c0387d/e7448/image31.png 900w, 800/static/06e8b844cd161cf27413aaf180c0387d/9ac8b/image31.png 1350w, 801/static/06e8b844cd161cf27413aaf180c0387d/28bdc/image31.png 1800w, 802/static/06e8b844cd161cf27413aaf180c0387d/7b4c8/image31.png 2438w" alt="" width="900" height="679"></picture></figure> 803<h1 id="results" style="position:relative;"><a href="#results" aria-label="results permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Results</h1> 804<p>So, how much did these optimizations help? In our test model with ~100 categories, implementing the optimizations (and enabling selective data loading) reduces the JavaScript cost by almost four times ð¤¯</p> 805<figure class="media-container"><picture><img src="/static/16b4323548d246d62f1dc830628db74e/e7448/image32.png" srcset="/static/16b4323548d246d62f1dc830628db74e/0608e/image32.png 225w, 806/static/16b4323548d246d62f1dc830628db74e/31401/image32.png 450w, 807/static/16b4323548d246d62f1dc830628db74e/e7448/image32.png 900w, 808/static/16b4323548d246d62f1dc830628db74e/9ac8b/image32.png 1350w, 809/static/16b4323548d246d62f1dc830628db74e/28bdc/image32.png 1800w, 810/static/16b4323548d246d62f1dc830628db74e/7b4c8/image32.png 2438w" alt="" width="900" height="679"></picture><figcaption class="media-container__caption">Before (median run out of 5)</figcaption></figure> 811<figure class="media-container"><picture><img src="/static/fb7a5d7b83b77d653e0e7c583494b9ff/e7448/image33.png" srcset="/static/fb7a5d7b83b77d653e0e7c583494b9ff/0608e/image33.png 225w, 812/static/fb7a5d7b83b77d653e0e7c583494b9ff/31401/image33.png 450w, 813/static/fb7a5d7b83b77d653e0e7c583494b9ff/e7448/image33.png 900w, 814/static/fb7a5d7b83b77d653e0e7c583494b9ff/9ac8b/image33.png 1350w, 815/static/fb7a5d7b83b77d653e0e7c583494b9ff/28bdc/image33.png 1800w, 816/static/fb7a5d7b83b77d653e0e7c583494b9ff/7b4c8/image33.png 2438w" alt="" width="900" height="679"></picture><figcaption class="media-container__caption">After (median run out of 5)</figcaption></figure> 817<p>With these optimizations, updating category cells becomes a much smoother experience:</p> 818<figure class="media-container"><video controls muted><source src="/a1acee387c261a6524adf6ed4014fc2e/image34.mp4" type="video/mp4"></video></figure> 819<div class="sidenote"><div class="sidenote__body"><p>We still have chunks of yellow/red in the recording, but theyâre much smaller â and interwined with blue!</p></div><blockquote class="sidenote__heading"><strong class="sidenote__remark">Sidenote:</strong> Yellow/red = main thread is busy. Blue = main thread is free.</blockquote></div> 820<p><em>Cross-posted <a href="https://www.causal.app/blog/react-perf">to the Causal blog</a>.</em></p></div></article><div class="styled__BottomMeta-sc-g87c0z-3 bWLCcb">Published<!-- --> <time dateTime="2022-11-24T09:00:00.000Z">24 November 2022</time></div><div class="styled__Container-sc-1pidu4x-0 fTqJyl styled__MailchimpSubscribe-sc-g87c0z-5 loPRVe"><p>Performance articles, case studies, and more. A new email every once in a while:</p><form id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="styled__MailchimpForm-sc-1pidu4x-1 YNXaB" action="https://iamakulov.us15.list-manage.com/subscribe/post?u=d7d2af05c405078e8f6a48956&amp;id=6c7cabbc15" method="post" target="_blank"><div><input placeholder="Your email" type="email" required="" class="styled__MailchimpInput-sc-1pidu4x-2 ealAEm" name="EMAIL" value=""/><input type="submit" class="styled__MailchimpSubmit-sc-1pidu4x-3 celNKA" value="Subscribe"/></div><div aria-hidden="true" class="styled__HiddenDiv-sc-1pidu4x-4 hSVmgK"><input type="text" tabindex="-1" name="b_d7d2af05c405078e8f6a48956_6c7cabbc15" value=""/></div></form></div><ul class="styled__Container-sc-exfs5j-0 jViMaS styled__BlogFooterAccordion-sc-g87c0z-6 jHbsYy"><li class="styled__Item-sc-exfs5j-1 bHlTQw"><a href="/talks/web-perf-101/" class="styled__ItemLink-sc-exfs5j-2 chErgn"><span class="styled__Container-sc-1ekqqun-0 ewhQKw"><picture><source srcSet="/static/ebdda6f2dea722987bbbfe64f77a7713/1de70/web-perf-101.webp 231w, 821/static/ebdda6f2dea722987bbbfe64f77a7713/89631/web-perf-101.webp 462w" sizes="231px" type="image/webp"/><img src="/static/ebdda6f2dea722987bbbfe64f77a7713/64756/web-perf-101.png" srcSet="/static/ebdda6f2dea722987bbbfe64f77a7713/64756/web-perf-101.png 231w, 822/static/ebdda6f2dea722987bbbfe64f77a7713/09d6e/web-perf-101.png 462w" sizes="231px" loading="lazy" alt="" width="231" height="130" class="styled__Img-sc-1ekqqun-1"/></picture></span><div class="styled__LabelWrapper-sc-exfs5j-3 lacEJS"><span class="styled__Label-sc-exfs5j-4 kZXEoD">Web Performance 101</span></div></a></li><li class="styled__Item-sc-exfs5j-1 bHlTQw"><a href="/talks/react-concurrency/" class="styled__ItemLink-sc-exfs5j-2 chErgn"><span class="styled__Container-sc-1ekqqun-0 ewhQKw"><picture><source srcSet="/static/6c41576701f7901f2be352b4a89146c3/1de70/react-concurrency.webp 231w, 823/static/6c41576701f7901f2be352b4a89146c3/89631/react-concurrency.webp 462w" sizes="231px" type="image/webp"/><img src="/static/6c41576701f7901f2be352b4a89146c3/64756/react-concurrency.png" srcSet="/static/6c41576701f7901f2be352b4a89146c3/64756/react-concurrency.png 231w, 824/static/6c41576701f7901f2be352b4a89146c3/09d6e/react-concurrency.png 462w" sizes="231px" loading="lazy" alt="" width="231" height="130" class="styled__Img-sc-1ekqqun-1"/></picture></span><div class="styled__LabelWrapper-sc-exfs5j-3 lacEJS"><span class="styled__Label-sc-exfs5j-4 kZXEoD">React Concurrency, Explained</span></div></a></li></ul><div class="styled__Container-sc-fmk9on-0 cuFkco styled__Footer-sc-g87c0z-7 kMmgzr"><div class="styled__Credentials-sc-fmk9on-1 qJRRG"><span><a href="/">PerfPerfPerf</a> · 2017â<!-- -->2026<span> <!-- -->· License:<!-- --> <span class="styled__LicenseName-sc-fmk9on-3 ecDLDl"><a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA</a></span></span></span><span>ð¤</span></div><div class="styled__Legal-sc-fmk9on-2 eMPHzj">Sole proprietorship (eenmanszaak). Amsterdam, Netherlands. KvK no. 86522469. VAT ID NL004265772B46.<!-- --> <a href="/legal/terms/">Terms and Conditions</a>.<!-- --> <a href="/legal/privacy/">Privacy Policy</a></div></div></div></div><!--/$--></div><div id="gatsby-announcer" style="position:absolute;top:0;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;
824border:0" aria-live="assertive" aria-atomic="true"></div></div>
824<script id="gatsby-script-loader">/*<![CDATA[*/window.pagePath="/blog/causal/";/*]]>*/</script>
824<!-- slice-start id="_gatsby-scripts-1" --> 825
825<script 826 id="gatsby-chunk-mapping" 827 > 828 window.___chunkMapping="{\"app\":[\"/app-7c7e0ed1706f68ec735d.js\"],\"component---src-pages-404-tsx\":[],\"component---src-pages-index-tsx\":[],\"component---src-pages-talks-quick-apps-index-tsx\":[],\"component---src-pages-talks-react-concurrency-index-tsx\":[],\"component---src-pages-talks-web-perf-101-index-tsx\":[],\"component---src-pages-workshops-index-tsx\":[],\"component---src-templates-blog-index-tsx\":[],\"component---src-templates-legal-index-tsx\":[],\"component---src-pages-index-tsxhead\":[\"/component---src-pages-index-tsxhead-2126d4c0e02f8903fe65.js\"],\"component---src-pages-talks-quick-apps-index-tsxhead\":[\"/component---src-pages-talks-quick-apps-index-tsxhead-30b771e3aa76436f65b9.js\"],\"component---src-pages-talks-react-concurrency-index-tsxhead\":[\"/component---src-pages-talks-react-concurrency-index-tsxhead-1031fb7d597c3ca2967f.js\"],\"component---src-pages-talks-web-perf-101-index-tsxhead\":[\"/component---src-pages-talks-web-perf-101-index-tsxhead-4b5467be3f8cb064501b.js\"],\"component---src-pages-workshops-index-tsxhead\":[\"/component---src-pages-workshops-index-tsxhead-df6431b0b285543a848f.js\"],\"component---src-templates-blog-index-tsxhead\":[\"/component---src-templates-blog-index-tsxhead-3fe94935e24b77395474.js\"],\"component---src-templates-legal-index-tsxhead\":[\"/component---src-templates-legal-index-tsxhead-d2339914f3d2831f6f13.js\"],\"node_modules-gatsby-link-dist-index-modern-mjs\":[],\"src-components-Logo-index-tsx\":[\"/src-components-Logo-index-tsx-b3f4f80591c34dfcdb67.js\"],\"node_modules-gatsby-script-dist-index-modern-mjs\":[],\"src-components-Layout-GlobalStyle-tsx\":[\"/src-components-Layout-GlobalStyle-tsx-7a4e3f3590993d818200.js\"],\"src-components-talks-AnimatedSlide-index-tsx\":[\"/src-components-talks-AnimatedSlide-index-tsx-d13f034165ea3f07dafe.js\"],\"cache-slice-js\":[]}"; 829 </script>
829 830
830<script>window.___webpackCompilationHash="5aa3833b247eab3dbd36";</script>
830<script src="/webpack-runtime-eaec3bb8172c552516a3.js" async></script>
830<script src="/framework-f2732454a48ad2e2fc2d.js" async></script>
830<script src="/app-7c7e0ed1706f68ec735d.js" async></script>
830<!-- slice-end id="_gatsby-scripts-1" --></body></html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.