PageSourceSearch

https://3perf.com/blog/causal/

html 3perf.com collected 2026-10-03 19:46:38 UTC 129,051 bytes, 830 lines download raw bytes

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:&quot;Open the React Profiler and <a href=&quot;https://share.cleanshot.com/ICgcZg&quot;>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.&quot;}" 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">&lt;</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">&amp;&amp;</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">&lt;</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">&lt;</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">&lt;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">&lt;</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">&lt;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">&lt;</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;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.