Skip to content

Commit 0106a70

Browse files
authored
[docs] Virtualize icons svg (#43939)
1 parent df7bb27 commit 0106a70

File tree

1 file changed

+74
-26
lines changed

1 file changed

+74
-26
lines changed

docs/data/material/components/material-icons/SearchIcons.js

Lines changed: 74 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import * as mui from '@mui/icons-material';
2525
import { Link } from '@mui/docs/Link';
2626
import { useTranslate } from '@mui/docs/i18n';
2727
import useQueryParameterState from 'docs/src/modules/utils/useQueryParameterState';
28+
2829
// For Debugging
2930
// import Menu from '@mui/icons-material/Menu';
3031
// import MenuOutlined from '@mui/icons-material/MenuOutlined';
@@ -95,6 +96,8 @@ function selectNode(node) {
9596

9697
const iconWidth = 35;
9798

99+
const SVG_ICON_CLASS = 'svg-icon';
100+
98101
const StyledIcon = styled('span')(({ theme }) => ({
99102
display: 'inline-flex',
100103
flexDirection: 'column',
@@ -108,23 +111,24 @@ const StyledIcon = styled('span')(({ theme }) => ({
108111
textAlign: 'center',
109112
width: `calc(${iconWidth}px + ${theme.spacing(2)} * 2 + 2px)`,
110113
},
111-
}));
112-
113-
const StyledSvgIcon = styled(SvgIcon)(({ theme }) => ({
114-
boxSizing: 'content-box',
115-
cursor: 'pointer',
116-
color: theme.palette.text.primary,
117-
border: '1px solid transparent',
118-
fontSize: iconWidth,
119-
borderRadius: '12px',
120-
transition: theme.transitions.create(['background-color', 'box-shadow'], {
121-
duration: theme.transitions.duration.shortest,
122-
}),
123-
padding: theme.spacing(2),
124-
margin: theme.spacing(0.5, 0),
125-
'&:hover': {
126-
backgroundColor: theme.palette.background.default,
127-
borderColor: theme.palette.primary.light,
114+
[`& .${SVG_ICON_CLASS}`]: {
115+
width: iconWidth,
116+
height: iconWidth,
117+
boxSizing: 'content-box',
118+
cursor: 'pointer',
119+
color: theme.palette.text.primary,
120+
border: '1px solid transparent',
121+
fontSize: iconWidth,
122+
borderRadius: '12px',
123+
transition: theme.transitions.create(['background-color', 'box-shadow'], {
124+
duration: theme.transitions.duration.shortest,
125+
}),
126+
padding: theme.spacing(2),
127+
margin: theme.spacing(0.5, 0),
128+
'&:hover': {
129+
backgroundColor: theme.palette.background.default,
130+
borderColor: theme.palette.primary.light,
131+
},
128132
},
129133
}));
130134

@@ -143,20 +147,58 @@ function handleLabelClick(event) {
143147
selectNode(event.currentTarget);
144148
}
145149

150+
function isElmVisible(elm, margin = 0) {
151+
const rect = elm.getBoundingClientRect();
152+
return rect.bottom >= -margin && rect.top <= window.innerHeight + margin;
153+
}
154+
146155
function Icon(props) {
147-
const { icon, onOpenClick } = props;
156+
const { icon, onOpenClick, initiallyVisible = false } = props;
157+
158+
const rootRef = React.useRef(null);
159+
const [isVisible, setIsVisible] = React.useState(initiallyVisible);
160+
161+
// Virtualize the icons to reduce page size and React rendering time.
162+
// Only render the icons after they become visible in the viewport.
163+
React.useEffect(() => {
164+
const margin = 200;
165+
const root = /** @type {SVGElement} */ (rootRef.current);
166+
if (initiallyVisible || isElmVisible(root, margin)) {
167+
setIsVisible(true);
168+
return () => {};
169+
}
170+
const observer = new IntersectionObserver(
171+
(entries) => {
172+
if (isElmVisible(entries[0].target, margin)) {
173+
setIsVisible(true);
174+
}
175+
},
176+
{ rootMargin: `${margin}px 0px` },
177+
);
178+
observer.observe(root);
179+
return () => {
180+
observer.disconnect();
181+
};
182+
}, [initiallyVisible]);
183+
148184
/* eslint-disable jsx-a11y/click-events-have-key-events */
149185
return (
150186
<StyledIcon
151187
key={icon.importName}
188+
ref={rootRef}
152189
onClick={Math.random() < 0.1 ? handleIconClick(icon) : null}
153190
>
154-
<StyledSvgIcon
155-
component={icon.Component}
156-
tabIndex={-1}
157-
onClick={onOpenClick}
158-
title={icon.importName}
159-
/>
191+
{isVisible ? (
192+
<SvgIcon
193+
component={icon.Component}
194+
className={SVG_ICON_CLASS}
195+
tabIndex={-1}
196+
onClick={onOpenClick}
197+
title={icon.importName}
198+
/>
199+
) : (
200+
<div className={SVG_ICON_CLASS} />
201+
)}
160202
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- TODO: a11y */}
161203
<div onClick={handleLabelClick}>{icon.importName}</div>
162204
{/* eslint-enable jsx-a11y/click-events-have-key-events */}
@@ -169,8 +211,14 @@ const Icons = React.memo(function Icons(props) {
169211

170212
return (
171213
<div>
172-
{icons.map((icon) => (
173-
<Icon key={icon.importName} icon={icon} onOpenClick={handleOpenClick} />
214+
{icons.map((icon, i) => (
215+
<Icon
216+
key={icon.importName}
217+
icon={icon}
218+
onOpenClick={handleOpenClick}
219+
// Render the first 50 icons immediately as they would be visible on page load
220+
initiallyVisible={i < 50}
221+
/>
174222
))}
175223
</div>
176224
);

0 commit comments

Comments
 (0)