Share
Embed
The dream that turned into nightmare (lightning)
Radoslav Stankov
July 20, 2024
Transcript
64 slides
-
"Dream" is short for "the dream to not write custom
CSS". Let's hope v2 is not named "Nightmare" # Goals - Have consistent UI on the website - Make it easier to build new pages - Implement Product Hunt v6 design - Fully type safe What - Foundational utilities - Core UI components -
"Dream" is short for "the dream to not write custom
CSS". Let's hope v2 is not named "Nightmare" # Goals - Have consistent UI on the website - Make it easier to build new pages - Implement Product Hunt v6 design - Fully type safe What - Foundational utilities - Core UI components $ -
Core Design System - 4px grid - font size -
font weights - color tokens - layouts -
import { Text } from '~/components/dream' <Text as="h1" /> <Text.Title>Title</Text.Title>
<Text.Subtile>Subtile<Text./Subtile> <Text.Bold>Bold</Text.Bold> -
import { Button } from 'ph/components/dream' <Button.Solid label="text" icon={TwitterIcon} />
<Button.Outline label="text" /> <Button.Text label="text" /> <Button.Icon icon={TwitterIcon} /> -
import { Button } from 'ph/components/dream' <Button.Solid to={paths.post(post)} /> <Button.Solid
onClick={fn} /> <Button.Solid mutation={GRAPHQL_MUTATION} /> <Button.Solid confirm="Are you sure" /> <Button.Solid requireLogin={true} /> -
import { Layout } from 'ph/components/dream' <Layout header={Layout.Header}> <Layout.Main />
<Layout.Aside /> </Layout> -
import { Form } from 'ph/components/dream' <Form.Mutation mutation={GRAPHQL_MUTATION} onSubmit={onSubmit}> <Form.Label
label="First name" flex={1}> <Form.Input name="first_name" placeholder="Placeholder" /> </Form.Label> <Form.Label label="Last name" flex={1}> <Form.Input name="last_name" placeholder="Placeholder" /> </Form.Label> <Form.Submit /> <Form.Status /> </Form.Mutation> -
<Text>{content}</Text> <Text fontSize={16}>{content}</Text> <Text fontSize={16} p={1}>{content}</Text> <Text fontSize={16} p={{ mobile:
1, tablet: 2, desktop: 3}}>{content}</Text> <Text fontSize={{ mobile: 12, tablet: 14, desktop: 16}} p={{ mobile: 1, tablet: 2, <div className="fontSize-14">{content}</div> <div className="fontSize-16">{content}</div> <div className="fontSize-16 p-1">{content}</div> <div className="fontSize-16 p-mobile-1 p-table-2 p-desktop-3">{content}</div> <div className="fontSize-mobile-12 fontSize-mobile-14 fontSize-deskopt-16 p-mobile -
<Text>{content}</Text> <Text fontSize={16}>{content}</Text> <Text fontSize={16} p={1}>{content}</Text> <Text fontSize={16} p={{ mobile:
1, tablet: 2, desktop: 3}}>{content}</Text> <Text fontSize={{ mobile: 12, tablet: 14, desktop: 16}} p={{ mobile: 1, tablet: 2, <div className="fontSize-14">{content}</div> <div className="fontSize-16">{content}</div> <div className="fontSize-16 p-1">{content}</div> <div className="fontSize-16 p-mobile-1 p-table-2 p-desktop-3">{content}</div> <div className="fontSize-mobile-12 fontSize-mobile-14 fontSize-deskopt-16 p-mobile -
<Text>{content}</Text> <Text fontSize={16}>{content}</Text> <Text fontSize={16} p={1}>{content}</Text> <Text fontSize={16} p={{ mobile:
1, tablet: 2, desktop: 3}}>{content}</Text> <Text fontSize={{ mobile: 12, tablet: 14, desktop: 16}} p={{ mobile: 1, tablet: 2, <div className="fontSize-14">{content}</div> <div className="fontSize-16">{content}</div> <div className="fontSize-16 p-1">{content}</div> <div className="fontSize-16 p-mobile-1 p-table-2 p-desktop-3">{content}</div> <div className="fontSize-mobile-12 fontSize-mobile-14 fontSize-deskopt-16 p-mobile -
<Text>{content}</Text> <Text fontSize={16}>{content}</Text> <Text fontSize={16} p={1}>{content}</Text> <Text fontSize={16} p={{ mobile:
1, tablet: 2, desktop: 3}}>{content}</Text> <Text fontSize={{ mobile: 12, tablet: 14, desktop: 16}} p={{ mobile: 1, tablet: 2, <div className="fontSize-14">{content}</div> <div className="fontSize-16">{content}</div> <div className="fontSize-16 p-1">{content}</div> <div className="fontSize-16 p-mobile-1 p-table-2 p-desktop-3">{content}</div> <div className="fontSize-mobile-12 fontSize-mobile-14 fontSize-deskopt-16 p-mobile -
<Text>{content}</Text> <Text fontSize={16}>{content}</Text> <Text fontSize={16} p={1}>{content}</Text> <Text fontSize={16} p={{ mobile:
1, tablet: 2, desktop: 3}}>{content}</Text> <Text fontSize={{ mobile: 12, tablet: 14, desktop: 16}} p={{ mobile: 1, tablet: 2, <div className="fontSize-14">{content}</div> <div className="fontSize-16">{content}</div> <div className="fontSize-16 p-1">{content}</div> <div className="fontSize-16 p-mobile-1 p-table-2 p-desktop-3">{content}</div> <div className="fontSize-mobile-12 fontSize-mobile-14 fontSize-deskopt-16 p-mobile -
<Text>{content}</Text> <Text fontSize={16}>{content}</Text> <Text fontSize={16} p={1}>{content}</Text> <Text fontSize={16} p={{ mobile:
1, tablet: 2, desktop: 3}}>{content}</Text> <Text fontSize={{ mobile: 12, tablet: 14, desktop: 16}} p={{ mobile: 1, tablet: 2, <div className="fontSize-14">{content}</div> <div className="fontSize-16">{content}</div> <div className="fontSize-16 p-1">{content}</div> <div className="fontSize-16 p-mobile-1 p-table-2 p-desktop-3">{content}</div> <div className="fontSize-mobile-12 fontSize-mobile-14 fontSize-deskopt-16 p-mobile -
<Text>{content}</Text> <Text fontSize={16}>{content}</Text> <Text fontSize={16} p={1}>{content}</Text> <Text fontSize={16} p={{ mobile:
1, tablet: 2, desktop: 3}}>{content}</Text> <Text fontSize={{ mobile: 12, tablet: 14, desktop: 16}} p={{ mobile: 1, tablet: 2, <div className="fontSize-14">{content}</div> <div className="fontSize-16">{content}</div> <div className="fontSize-16 p-1">{content}</div> <div className="fontSize-16 p-mobile-1 p-table-2 p-desktop-3">{content}</div> <div className="fontSize-mobile-12 fontSize-mobile-14 fontSize-deskopt-16 p-mobile -
<Text>{content}</Text> <Text fontSize={16}>{content}</Text> <Text fontSize={16} p={1}>{content}</Text> <Text fontSize={16} p={{ mobile:
1, tablet: 2, desktop: 3}}>{content}</Text> <Text fontSize={{ mobile: 12, tablet: 14, desktop: 16}} p={{ mobile: 1, tablet: 2, <div className="fontSize-14">{content}</div> <div className="fontSize-16">{content}</div> <div className="fontSize-16 p-1">{content}</div> <div className="fontSize-16 p-mobile-1 p-table-2 p-desktop-3">{content}</div> <div className="fontSize-mobile-12 fontSize-mobile-14 fontSize-deskopt-16 p-mobile % & -
interface IProps extends ITextCSSProps { children: React.ReactNode; className?: string; as?:
'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'strong' | 'p' | 'div' | 'i' | 'legend' | 'details' | 'footer' | 'summary'; id?: string; onClick?: React.MouseEventHandler; format?: boolean; noOfLines?: INoOfLines; center?: boolean; target?: '_blank' | '_self'; 'data-test'?: string; 'aria-label'?: string; ellipsis?: boolean; } export default function Text({ children, className, as, id, color = 'darker-grey', fontSize = 16, fontWeight = 400, format = false, center = false, noOfLines, 'data-test': dataTest, onClick, ellipsis, ...otherProps }: IProps) { const finalClassName = classNames( dreamCSSClasses({ ...otherProps, color, fontSize, fontWeight, noOfLines }), className, format && styles.format, center && styles.center, ellipsis && styles.ellipsis, ); const Component = as ?? 'div'; return ( <Component className={finalClassName} id={id} data-test={dataTest} onClick={onClick}> {children} </Component> ); } -
export function deprecatedDreamCSSClasses(props: IDreamCSS) { let classes: string = '';
const propNames = Object.keys(props); for (const propName of propNames) { const propValue = props[propName]; if (!ALLOWED_PROPS[propName]) { continue; } if (typeof propValue === 'object') { classes = classesForObject(classes, propName, propValue); } else { classes = classesForProps(classes, propName, propValue); } } return classes.trim(); } function classesForObject(classes: string, prop: string, props: any) { if (isNotSet(props.widescreen) && props.desktop) { props.widescreen = props.desktop; } -
function classesForObject(classes: string, prop: string, props: any) { if (isNotSet(props.widescreen)
&& props.desktop) { props.widescreen = props.desktop; } if (isNotSet(props.tablet) && props.mobile) { props.tablet = props.mobile; } const modifiers = Object.keys(props); for (const modifier of modifiers) { const propValue = props[modifier]; if (modifier === 'l') { classes = classesForProps(classes, prop, propValue); } else { classes = classesForProps(classes, `${prop}-${modifier}`, propValue); } } return classes; } function classesForProps(classes: string, propName: string, propValue: any) { if (typeof propValue === 'boolean' && propValue) { classes += `${propName}-default `; } else { -
const modifiers = Object.keys(props); for (const modifier of modifiers) {
const propValue = props[modifier]; if (modifier === 'l') { classes = classesForProps(classes, prop, propValue); } else { classes = classesForProps(classes, `${prop}-${modifier}`, propValue); } } return classes; } function classesForProps(classes: string, propName: string, propValue: any) { if (typeof propValue === 'boolean' && propValue) { classes += `${propName}-default `; } else { classes += `${propName}-${propValue} `; } return classes; } function isNotSet(value) { return typeof value === 'undefined' || value === null; } -
' Problem #1 - CSS bundle size All possible class
names must be pre-generated and included in the CSS bundle because they are generated dynamically. -
' Problem #1 - CSS bundle size gap: 14 sizes
* 5 variants = 46 margin: 7 props * 14 sizes * 5 variants = 350 padding: 7 props * 14 sizes * 5 variants = 350 colors: 2 props * 12 colors * 5 variants = 120 ... ... total: ~2000 class names ( -
) Problem #2 - too many loops dreamCSSClasses was called
for many components to build their class names. This results in loops and variable generation, resulting in many GCs and slowing rendering. Especially the initial rendering. -
) Problem #2 - too many loops dreamCSSClasses was called
for many components to build their class names. This results in loops and variable generation, resulting in many GCs and slowing rendering. Especially the initial rendering. -
) Problem #2 - too many loops 2000+ calls to
dreamCSSClasses for single render -
* Problem #3 - too many props Core components like
Text and Flex, accepted subsets of dreamCSSClasses props, leading to overcomplicated code. -
interface IProps extends ITextCSSProps { children: React.ReactNode; className?: string; as?:
'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'strong' | 'p' | 'div' | 'i' | 'legend' | 'details' | 'footer' | 'summary'; id?: string; onClick?: React.MouseEventHandler; format?: boolean; noOfLines?: INoOfLines; center?: boolean; target?: '_blank' | '_self'; 'data-test'?: string; 'aria-label'?: string; ellipsis?: boolean; } export default function Text({ children, className, as, id, color = 'darker-grey', fontSize = 16, fontWeight = 400, format = false, center = false, noOfLines, 'data-test': dataTest, onClick, ellipsis, ...otherProps }: IProps) { const finalClassName = classNames( dreamCSSClasses({ ...otherProps, color, fontSize, fontWeight, noOfLines }), className, format && styles.format, center && styles.center, ellipsis && styles.ellipsis, ); const Component = as ?? 'div'; return ( <Component className={finalClassName} id={id} data-test={dataTest} onClick={onClick}> {children} </Component> ); }