Core fundamentals

Box Model

Everything on the browser is a box.

mermaid
flowchart TD
    subgraph "Margin Box"
        subgraph "Border Box"
            subgraph "Padding Box"
                subgraph "Content Box"
                end
            end
        end
    end

Box Size

  • Intrinsic size: the size of the content box, depends on the content.

  • Restricted size: the size of the content box, depends on the parent element.

box-sizing property

When we set a width of 800, padding of 20, border of 5, and margin of 10, the total width will be:

  • content-box
    5px border left + 20px padding left + 800px content + 20px padding right + 5px border right = 850px

  • border-box
    5px border left + 20px padding left + (800px - 50px = 750px) content + 20px padding right + 5px border right = 850px

Box Type

block

  • Block context formatting

  • Top to bottom, left to right formatting

  • Takes the full width of the parent element

  • Height depends on the content (intrinsic height)

inline

  • Inline context formatting

  • Top to bottom, left to right formatting

  • Does not respect height and width properties.

  • Does not respect vertical margin and padding.

  • Only line-height can alter the height of the element.

  • Wraps text into a new line, when it overflows the parent element.

inline-block

Same as block but wraps text into a new line, when it overflows the parent element.

anonymous

Text that is not wrapped in any tags are treated as anonymous block elements.

html
<p>Hi</p>
Iam a anonymous block element

Browser formatting context

  • Formatting context family (block, flex, grid, inline, table, etc)

  • Elements inside a formatting context (eg, flex) is not affected by the outside formatting context.

mermaid
flowchart TD
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
    classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;

    subgraph "Block Formatting Context"
        subgraph "Block Formatting Context"
            subgraph "Flex Formatting Context"
                block[Block Formatting Context]:::yellow
                block1[Block Formatting Context]:::yellow
                block2[Block Formatting Context]:::yellow
                subgraph "Block Formatting Context"
                    p_tag[Inline Formatting Context\np-tag]:::red
                end
            end
        end
    end

Items inside the flex formatting context are not affected by the outer (block formatting) context.

Browser positioning system

  • The normal flow of elements is top to bottom, left to right.

  • By using the position property, we can change the normal flow of elements.

  • The position property can be static, relative, absolute, fixed, sticky.

  • Any position property other than static will create a new stacking context.

mermaid
flowchart TD
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
    classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;

    subgraph "Containing block - browser viewport"
        subgraph "html tag"
            subgraph "Containing block - relative position"
                absolute[absolute position]:::red
            end
        end
    end
  • The containing block is the parent element that the child element is positioned relative to.

  • The default containing block is the browser viewport.

  • The default position for an element is top left of the parent element.

Reflow

mermaid
flowchart TD
    HTML --> DOM
    CSS --> CSSOM
    DOM --> render_tree[Render Tree]
    CSSOM --> render_tree --> javascript[JavaScript]

    subgraph Reflow
        direction RL
        javascript -->
        style_m[Style] -->
        layout[Layout]
    end

    layout --> paint[Paint]

    subgraph Repaint
        paint --> composite[Composite]
    end
  • Reflow is the process of recalculating the position and size of elements in the DOM.

  • Reflow is triggered when the DOM or CSSOM is modified, and it leads to cascading layout changes. (for example margin-top of an element is changed, it will affect the position of all the elements below it)

  • The layout calculations are CPU intensive, and they block the render thread.

  • Once the layout is caclulated, the browser will paint the pixels to the screen.

  • And multiple layers are composited together to create the final image, seen on screen.

  • The repaint and composite phases are GPU intensive, and they do not block the render thread.

The below example is CPU intensive, since layout needs to be re-calculated.

scss
// non-optimised reflow
@keyframes moving-down-slow {
	from {
		margin-top: 0;
	}
	to {
		margin-top: 100px;
	}
}

The below example is GPU intensive, since the layout does not need to be re-calculated. Only the element needs to be repainted.

scss
// optimsied reflow
@keyframes moving-down-fast {
	from {
		transform: translateY(0);
	}
	to {
		transform: translateY(100px);
	}
}

Composition Layers

mermaid
flowchart TD
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
    classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;

    html[HTML\n<b>Render layer</b> created]:::yellow --> body --> main
    main --> div1[Div\ntransparent bg\n<b>Render layer</b> created]:::yellow
    main --> div2[Div\ntransform used\n<b>Render layer</b> created]:::yellow
    main --> div3[Div\nCSS filter\n<b>Render layer</b> created]:::yellow
    main --> div4[Div\n<b>visually hidden</b>\nNo <b>renderObject</b>]:::orange

Render object

  • Each element in the DOM tree is represented by a render object.

  • The render object tells the GPU how to draw the element.

  • Elements that are hidden are skipped ( no render object ).

Render layer

  • A render layer is created when an element has a property that requires it to be rendered separately from the rest of the page.

  • Videos, CSS filters, transforms, html tags, canvas, position other than static or relative, will render a new layer.

  • The Render layer is not necessarily GPU accelerated.

  • The Render layer holds data in RAM.

Graphic layer

  • Constructed when we use 3D, 2D transforms.

  • Elements that need to be GPU accelerated are moved to the graphic layer. (video with accelerated decoding, canvas with 3D/2D context, certain CSS filters, CSS animations that use opacity, transform, filter)

  • Graphic layers hold vRAM.

  • Too many graphic layers can cause performance issues.

mermaid
flowchart LR
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
    classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;

    dom[DOM Element] --> | if visible | render_obj[Render Object]:::red --> |has a diff \n stacking context| render_layer[Render Layer]:::orange --> |If GPU\naccelaration is needed|graphic_layer[Graphic Layer]:::yellow
    html[HTML / Root] -->  render_obj_1[Render Object]:::red --> |Always created for\nroot element| render_layer_1[Render Layer]:::orange --> |Always created for\nroot element| graphic_layer_1[Graphic Layer]:::yellow

DOM API

mermaid
classDiagram
    class HTMLDocument
    class TextNode
    class Node
    class Element
    class HTMLElement

    class DOMAPI {
        +querySelector()
        +querySelectorAll()
        +getElementById()
        +getElementsByClassName()
        +getElementsByTagName()
    }

    HTMLElement <|-- Element: extends
    Element <|-- Node: extends
    Node <|-- TextNode: extends
    Node <|-- HTMLDocument
    DOMAPI <..HTMLDocument: usage
    DOMAPI <..Element: usage

For some weird reason, both HTMDocument and Element have access to the DOMAPI methods.

ts
$0.querySelector;
document.querySelector;

Global objects

ts
console.log(typeof window); // object
console.log(window.document === document); // true
console.log(window.document.body); // <body></body>
console.log(window.document.head); // <head></head>

DOM Querying

getElementById

ts
document.getElementById('id'); // O(1)
  • O(1) time complexity

  • Id namespace is shared, across all components.

  • Make suer we dont overuse ids.

getElementsByTagName or getElementsByClassName

ts
document.getElementsByClassName('class'); // O(N)
  • O(n)T time complexity, performs a DFS on the DOM tree.

  • getElementByClassName is memoised

  • Returns a live HTMLCollection

  • HTMLCollection is a collection of elements that are in the DOM tree.

  • The collection is live, meaning that it will update when the DOM tree is updated.

  • Memory cost is very low, since it is a reference to the DOM tree O(1)S.

querySelector

ts
document.querySelector('.class'); // O(N)
  • The query selector uses a CSS selector to find elements.

  • memoised by the browser.

  • Depending on the selector complexity, it can be O(1)T or O(n)T.

  • Memory cost is low, since it is a reference to the DOM tree O(1)S.

querySelectorAll

ts
document.querySelectorAll('.class'); // O(N)
  • Returns a node list, returns a copies of the HTML elements

  • When the copied element is updated, the actual element is also updated.

  • O(N)ST.

  • The node list is not live, meaning that it will not update when the DOM tree is updated.

Performance optimisations

CSS Selectors

scss
// this is slow, since we need traverse longer
div > div > section > .flex > .target

// this is faster (When # is used we can skip the DOM tree traversal, upto the id)
#container.flex > .target
  • Complex queries take longer to compute

  • Use Id's for core containers

  • CSS selectors are compiled by the browser on run time.

Inserting new elements

Method
Perf impact

innerHtml = <div></div>

Extremely slow

innerAdjacentHtml

Extremely slow

insertAdjacentElement

High impact

appendChild

Low impact

innerHtml and innerAdjacentHtml will invoke the dom parser, and will re-parse the entire DOM tree, and does a reflow.

Removing elements

innerHtml = '' will remove all the elements in the DOM tree. removeChild will remove the element from the DOM tree.

HTML <template>

  • Template is a special tag that is not rendered in the DOM tree.

  • Not accessible through the DOM API's by external api's.

  • DocumentFrament is a lightweight version of the HTMLElement representation.

  • It's isolated from the DOM tree, any changes to the DocumentFragment will not affect the DOM tree and doesn't cause reflow.

  • We can use HTML to create a markup of component.

html
<!doctype html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		<title></title>
		<link href="./index.css" rel="stylesheet" />
	</head>
	<body>
		<template id="template">
			<div class="card">
				<h3 class="card-title"></h3>
				<div class="card-content">
					<p class="card-desc"></p>
					<button>Click me</button>
				</div>
			</div>
		</template>
	</body>
	<script src="./index.js"></script>
</html>
js
const template = document.getElementById('template');

const createAndAppendCard = (title, desc) => {
	const card = template.content.cloneNode(true).firstElementChild;
	// cloneNod(isDeepClone?) deep clone to avoid nested elements from being modified by other clones
	// template.content.cloneNode(true) returns a DocumentFragment
	// firstElementChild gets the first child of the template / documentFragment
	const titleEl = card.querySelector('.card-title');
	const descEl = card.querySelector('.card-desc');

	titleEl.innerText = title; // will cause reflow avoid
	descEl.textContent = desc; // will not cause reflow

	document.body.appendChild(card);
	// O(N), causes re-flow
};

createAndAppendCard('Title 1', 'Description 1');

DocumentFragment for dynamic templates

js
const fragment = new DocumentFragment();
for (let idx = 0; idx < 1000; idx++) {
	const card = template.content.cloneNode(true).firstElementChild;
	const titleEl = card.querySelector('.card-title');
	const descEl = card.querySelector('.card-desc');

	titleEl.innerText = `Title ${idx}`;
	descEl.textContent = `Description ${idx}`;

	fragment.appendChild(card);
}
document.body.appendChild(fragment); // this operation causes re-flow only once

Web APIs for Complex UI Patterns

Observer APIs

Intersection Observers

ts
const callback = (entries, observer) => {
	entries.forEach(entry => {
		if (entry.isIntersecting) {
			console.log('Element is in view');
		} else {
			console.log('Element is out of view');
		}
	});
};

const observer = new IntersectionObserver(callback, {
	threshold: 0.5,
	root: document.querySelector('.container'),
});

observer.observe(document.querySelector('.target'));
  • Lazy loading

  • Tracks intersection on an element with the viewport

  • An observer can observe multiple elements

  • We can also set the containing element to observe the intersection

  • Intersection event is handled on the native thread instead of the event thread.

Mutation Observers

  • Drawing tools

  • Rich text editors

ts
type MutationRecord = {
	type: 'childList' | 'attributes' | 'subtree' | 'characterData';
	target: Node; // the node that was changed
	addedNodes: NodeList; // nodes added with notation
	removedNodes: NodeList; // nodes removed with notation
	oldValue: string; // prev value
};

const callback = ((mutationsList) => {
	mutationsList.forEach(mutation: MutationRecord) => {
        // do something with the mutation
	});
};

let observer = new MutationObserver(callback);
observer.observe(document.querySelector('.target')!, {
	childList: true, // tracks only immediate children, has higher priority over subTree
	attributes: true, // track attribute changes
	subtree: true, // track changes in descendants
	characterData: true, // tracks text changes
	attributesFilter: ['class', 'id'],
});

We can use replaceWith to replace the target element with a new element.
We can use document.createElement to create a new element.

ts
h1 = document.createElement('h1');
h1.textContent = textContent.slice(3);
target.replaceWith(h1);

Resize Observers

scss
.blog-section {
	width: 100%; // width of the container should not be auto
	margin-bottom: $margin-bottom--6;
	container: blog-section-container;
	container-type: inline-size;

	&__title {
		margin-bottom: $margin-bottom--2;
	}
}

@container blog-section-container (max-width: 400px) {
	.blog-section {
		&__title {
			color: red;
		}
	}
}

Window resize

  • CSS media queries (very fast, no-callback, no element-tracking)

  • CSS container queries (very fast, no-callback, element-tracking)

  • "resize" event (very slow, because of even propogation, and frequent firing of events)

  • ResizeObserver (fast, 10x faster than resize event, but still slower than CSS media queries)

ts
const container = document.querySelector('.container');
// we may need to de-bounce the resize event,
// since it can fire multiple times in a short period of time, on resize
const resizeObserver = new ResizeObserver(entries => {
	for (let entry of entries) {
		const { width, height } = entry.contentRect;
		console.log(`Container resized to ${width}px x ${height}px`);
	}
});

resizeObserver.observe(container);

Virtualization

  • If we have a lot of elements that we need render as a list.

  • We can use vitalization to render only the DOM elements visible in the viewport.

  • By limiting the no of nodes on the DOM tree, we can improve performance.

  • We can use IntersectionObserver recycle the elements with new content. The top and bottom elements are recycled, when the user scrolls to the ends.

  • By using a transform-translate property, the element is promoted to the graphics layer.

  • Removing and adding elements in the GPU layer, makes sure that only those element needs to be re-painted, avoiding cascading reflow.

Application state & Network connectivity

Application state design

mermaid
flowchart TD
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
    classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;


    data_classes[Data Classes]
    data_classes --> |themes,\nfont size|config[App Configs]
    data_classes --> |input-value,\nselected state bold, italic|ui_elements_state[UI Elements State]
    data_classes --> |open ports\nmessages|server_data[Server Data]

    data_properties[Data Properties]
    data_properties --> access_levels[Access Level]
    data_properties --> read_write_freq[Read Write Frequency]
    data_properties --> data_size[Data Size]

How to optimise the data structure for the application state.

  • Minimize access cost, make sure its O(1)

  • Minimize search cost, make sure its fast

  • Minimize memory usage (RAM)

Data Normalization (1NF, 2NF, 3NF)

ts
// 1NF,
// all keys use a single atomic value
const user_id_1 = {
        id: 1, // primary key
        name: 'John', // atomic
        age: 30,
        address_street: '123 Main St',
        address_city: 'New York',
        address_state: 'NY',
        address_zip: '10001'
};

// 2 NF,
const users = {
    1: {
        id: 1, // primary key
        name: 'John', // name depends on id, primary key
        age: 30,
        address: {
            street: '123 Main St',
            // depends on address, address depends on id
            city: 'New York',
            state: 'NY',
            zip: '10001'
        }
    },
]

// 3 NF, (usually excessive on the FE)
const addresses: {
    1: {
            street: '123 Main St',
            city: 'New York',
            state: 'NY',
            zip: '10001'
    }
};

const users = {
    1: {
        id: 1,
        name: 'John',
        age: 30,
        address: addressee[1] // no transitive dependency
    },
]

Reduce search cost

  • Session storage, when the user closes the website, data is wiped. (5MB limit)

  • Local storage, provides persistence, 5MB limit, only strings are allowed.

  • Used indexes if in-app search is required.

  • Indexed DB, doesn't block the UI thread, unlimited data, allows indexing.

Network Connectivity

mermaid
classDiagram
    class UDP
    class TCP

    class QUIK
    class WebRTC
    class http3 {
        <<limited server support>>
    }
    class http_1_1 {
        as "HTTP 1.1"
    }
    class http2
    class ServerSentEvents
    class WebSockets

    UDP <|-- QUIK : over
    UDP <|-- WebRTC : over
    QUIK <|-- http3
    TCP <|-- http_1_1 : over
    http_1_1 <|-- http2
    http2 <|-- ServerSentEvents
    http_1_1 <.. WebSockets

    note for WebSockets "WebSockets use HTTP1 for inital connection ony, \nlater they use TCP natively"

    note for WebRTC "WebRTC is used for video/audio streaming"

    not for
  • HTTP 1.1 doesn't compress headers.

  • HTTP 2 compress headers,

Why is long polling bad?

  • TCP 3 way hand shack is resource intensive, CUP utilization is high.

  • Battery consumption on mobile devices.

    • Mono mode on mobile network module, is very energy efficient. (Only for receiving)

    • Duplex mode, consumes a lot of energy, 3.7hrs is enough to run through 5 minutes of short polling with 5 minutes of timeout. (For both sending and receiving)

  • When a user with a mobile device is traveling, the TCP connection needs to be re-established everytime the user connects to a new cell tower.

  • For desktop users its very well suited.

SSE

  • TCP handshake duplex antenna is used.

  • Data sent back from server uses Mono mode.

  • Re-connection with cell towers is handled automatically.

Websockets

  • Once TCP handshake is established

  • We upgraded to Web-Sockets, which implements TCP connection natively

  • Infra, and Engineering heavy

  • Re-connection is not implemented by ISP's.

  • Its stateful

  • Requires duplex antenna.

  • Realtime communications.

  • When using a chat app, we can use SSE to receive messages, and can use REST for sending messages.

REST / GraphQL

Consequences of using GraphQL

  • We add client library for using GraphQL API

  • Additional client caching layer

  • Additional state management, GraphQL client is responsible for syncing state between client and server.

  • Increased bundle size

When do want to use GraphQL?

mermaid
flowchart TD
    A[Application Size] --> B[Small]
    A --> C[Medium]
    A --> D[Large]

    B --> E[Classic REST]

    C --> F{Widely used public API}
    D --> F

    F -- Yes --> E1[Classic REST]
    F -- No --> G{Most of your requests are readonly}

    G -- Yes --> H{Limited Budget / Team size}
    H -- Yes --> E2[Classic REST]
    H -- No --> I{Isomorphic Data types}

    G -- No --> I

    I -- No --> E3[Classic REST]
    I -- Yes --> J{Bundle Size critical}

    J -- Yes --> E4[Classic REST]
    J -- No --> K{Complex API Model}

    K -- No --> E5[Classic REST]
    K -- Yes --> L[GraphQL]

GraphQL can reduce complexity,

  • GraphQLs useSubscription can be used to subscribe to wss, SSE, Short polling and Long polling, if your app uses a mix of them, its a good candidate for GraphQL.

Perfomace Optimisation

Web-vitals

LCP, (Largest Contentful Paint)

  • Loading performace of app

  • Should be < 2.5s

INP, (Interaction to Next Paint)

  • Rectivity should be < 200ms

CLS, (Cumulative Layout Shift)

  • Visual stability of the app

  • Layout shifts should be minimised to reduce reflows

  • < 0.1

Network performance

  • HTTP support

    • HTTP/1.1, 20% server usage support

    • HTTP/2, 50% usage

    • HTTP/3, 30% usage

  • HTTP 1, is limited to no of connections, browsers had a workaround to open multiple TCP connections, with a limit of 5.

    • Webpack was used initially to bundle everything into a single file, and it can be fetched in a single TCP connection.

    • HTTP 1 needs to open a new TCP connection for every request to a same domain.

    • Sends header in non-compressed format.

    • We had to host files in different domain to go around the per domain TCP limit.

  • HTTP 2, implements TCP pooling, same TCP connection can be used to pull multiple data from the same domain, in parallel.

    • HTTP 2 is backward compatible in HTTP 1

    • HTTP 2 compresses header

    • HTTP 2 allows multiplexing, within the same channel, we can send multiple resources in parallel.

Do I need polyfills

  • ES5 bundle size is very high, due to poly-filling

  • Supports

    • ES6, has 98.2% support

    • ES7-10, has 96% support

    • ES11, has 90%

    • ES12, 89% support

  • Pollyfills are needed only for a minority of clients

mermaid
flowchart TD
    JSCompiler[JS Compiler]
    ES5[es5 bundle]
    ES6_10[es6-10 bundle]
    ES2024[es2024 bundle]
    Server[Server]
    Client[Client]

    JSCompiler --> ES5
    JSCompiler --> ES6_10
    JSCompiler --> ES2024

    ES5 --> Server
    ES6_10 --> Server
    ES2024 --> Server

    Server -->|optimized html with js assets| Client
    Client -->|user-agent| Server

Split the bundle

  • We should be splitting code into different modules, and load modules (lazy) when needed.

  • main should be split into different bundles, (bundle1.js, bundler2.js`<)

  • Background fetches

    • <link rel="preload">, high priority background fetches.

    • <link rel="prefetch">, loads and caches in the background.

Minify

  • Code minification reduces file size by 20%.

js
function helloWold() {
    const longVariableName = 5 + 5,
    return longVariableName;
}

function h(){return 5+5}

Compress

  • gzip / broti, can be used to reduce the bundle size even further.

  • broti has a lower client support.

Change order of script loading

html
<script src="module1.js"></script>
<script src="anlaytics.js" defer></script>
<script src="module1.js"></script>

Tells the browser that we dont need the analytics.js module immediately, our app can function without it, we should load it later.

CSS can be minified and compressed as well.

mermaid
flowchart TD
    JSTranspiler[JS Transpiler]
    Mobile[Mobile]
    Standard[Standard]
    SuperWide[Super-wide]
    Server[Server]
    Client[Client]

    JSTranspiler --> Mobile
    JSTranspiler --> Standard
    JSTranspiler --> SuperWide

    Mobile --> Server
    Standard --> Server
    SuperWide --> Server

    Server -->|optimized html with css assets| Client
    Client -->|user-agent| Server

Inline critical styles

html
<html>
	<head>
		<link rel="stylesheet" href="href://cdn.com/desktop.css" />
	</head>
</html>

instead do this

html
<html>
	<head>
		<style>
			/* critical styles */
		</style>
		<link rel="stylesheet" href="href://cdn.com/desktop.css" />
	</head>
</html>

Fetching non-critical styles

html
<html>
	<head>
		<link
			rel="stylesheet"
			href="href://cdn.com/desktop.css"
			media="print"
			onload="this.media='all'"
		/>
	</head>
</html>
  • the media attribute is set to print, so the browser will not load the css file, until the page needs to be printed.

  • The onload event is used to set the media attribute to all, so the css file will be loaded when the page is loaded.

html
<link rel="preload" as="style" href="href://cdn.com/desktop.css" />
  • Alternatively we can use preload to load the css file in the background.

  • This is fired immediately, when the browser reads this line.

  • If we use prelaod too much, the all the downloads will be queued.

Use correct image formats

mermaid
flowchart TD
    Images[Images]
    Animated[Animated Content]
    Icons[Icon and Logos]
    Raster[Raster Graphic]
    Photos[Photos]

    GIF[GIF]
    MP4[MP4]
    SVG[SVG]
    CompressedSVG[Compressed SVG]
    PNG[png]
    JPEG[jpeg]
    WEBP[webp]
    AVIF[AVIF]

    Images --> Animated
    Images --> Icons
    Images --> Raster
    Images --> Photos

    Animated --> GIF
    GIF --> MP4

    Icons --> SVG
    SVG --> CompressedSVG

    Raster --> PNG
    PNG --> WEBP

    Photos --> JPEG
    JPEG --> WEBP
    WEBP --> AVIF
  • Compress SVG's

  • Use AVIF for better performance

Fonts

css
@font-face {
	font-family: 'MyFont';
    src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
	font-display: swap;
}
  • Use font-display: auto waits 3s to load the font, if the font is not loaded, it will use the fallback font. (Default in vanilla js)

  • Use font-display: swap or fallback to load the font in the background, once a font is loaded, it will be used. (Default in nextJs)

  • Use font-display: optional to load the font in the background, cached, is applied only on reload.

Summary

mermaid
graph TD
    Rendering
    Rendering --> UserPerception["User Perception"]
    Rendering --> DOM

    UserPerception --> Placeholders["Use Placeholders and loaders for network latency"]

    DOM --> MinDOM["Minimize number of DOM Elements"]
    MinDOM --> Virtualization["Use Virtualization when nessary"]

    DOM --> MinGraphicLayers["Minimize Number of Graphic Layers"]

    DOM --> AvoidReflow["Avoid Reflow"]
    AvoidReflow --> AvoidCSSSelectors["Avoid Complex CSS Selectors"]
    AvoidReflow --> CSSAnimation["Use CSS Animation"]

JS performance improvements

meramid
graph TD
    Javascript
    Javascript --> ReduceCPU["Reduce CPU Usage"]
    Javascript --> ReduceRAM["Reduce RAM usage"]
    Javascript --> DontBlockUI["Don't block UI thread"]
    Javascript --> AsyncJobs["Utilize Async jobs"]

    ReduceCPU --> MinimizeSearch["Minimize Search and access cost"]
    ReduceRAM --> MinimizeRuntime["Minimize runtime state"]
    MinimizeRuntime --> IndexedDB["Utilize IndexedDB"]

    DontBlockUI --> AvoidRW["Avoid high frequency read-write to sync storages\nlocal storage or \nsession storage"]

    AsyncJobs --> UseServer["Use server"]
    AsyncJobs --> WebWorkers["Use Web Workers"]

System design interviews

Start with the requirements

Functional requirements

  • Stories are rendered in a list, from top to bottom.

  • No of stories can be infinite.

  • Story can have media content (images right now)

Non-functional requirements

  • Works on mobile and desktop.

  • We should be network efficient.

  • We need to think about the performance of the app, (we shouldn't consume too much memory and CPU).

  • Offline support.

Virtualisation

  • We can use virtualization to render only the elements that are visible in the viewport.

API

Data model

ts
const stories = {
	story_id: {
		text: 'Story text',
		story_id: 'story_id',
		timestamp: 'timestamp',
	},
};

const comments = {
	story_id: {
		comment_id: {
			text: 'Comment text',
			id: 'id',
			timestamp: 'timestamp',
		},
	},
};

// 2nf, 3nf looks excessive
const attachments = {
	story_id: {
		attachment_id: {
			url: 'url',
			type: 'image',
			timestamp: 'timestamp',
		},
	},
};

API Methods

No title

GET /stories?limit=10&end_id=90

Short polling / Long polling

  • Battery drain

  • Data inefficient

  • Latency (TCP re-connection)

  • Simple

SSE

  • Efficient

  • Fast

  • Latency is high

Websocket,

  • Realtime communication

  • Fast

  • Infra and engineering heavy

Performance optimisation

Network

  • HTTP2

  • Split bundles

  • Platform specific bundles

  • Defer the script loading

  • Minified

  • Compress GZIP / Brotli

  • CDN is used for static assets

  • We should use optimised image formats (webp, avif)

  • Font loading

  • API that returns optimised viewport image based on device size

Rendering

  • Limit no of DOM elements ( use virtualization )

  • Reduce CSS selector complexity (Try to flatten it)

  • Minimise Reflows (Use CSS transforms, for animations)

  • Loading placeholders

JS

  • Don't block the UI thread

  • Offload heavy tasks to web workers or API calls

  • Use requestIdleCallbacks

  • Use async storage

  • Use service workers, to cache network requests, offload work.

  • Offload run-time state to IndexedDB