Box Model
Everything on the browser is a box.
flowchart TD
subgraph "Margin Box"
subgraph "Border Box"
subgraph "Padding Box"
subgraph "Content Box"
end
end
end
endBox 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-box5px border left + 20px padding left + 800px content + 20px padding right + 5px border right = 850px
border-box5px 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.
<p>Hi</p>
Iam a anonymous block elementBrowser 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.
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
endItems 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.
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
endThe 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
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]
endReflow 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.
// 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.
// optimsied reflow
@keyframes moving-down-fast {
from {
transform: translateY(0);
}
to {
transform: translateY(100px);
}
}Composition Layers
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>]:::orangeRender 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.
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]:::yellowDOM API
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: usageFor some weird reason, both HTMDocument and Element have access to the DOMAPI methods.
$0.querySelector;
document.querySelector;Global objects
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
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
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
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
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
// 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 > .targetComplex queries take longer to compute
Use Id's for core containers
CSS selectors are compiled by the browser on run time.
Inserting new elements
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.
<!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>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
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 onceWeb APIs for Complex UI Patterns
Observer APIs
Intersection Observers
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
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.
h1 = document.createElement('h1');
h1.textContent = textContent.slice(3);
target.replaceWith(h1);Resize Observers
.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)
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
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)
// 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
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 forHTTP 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?
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
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| ServerSplit 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%.
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
<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.
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| ServerInline critical styles
<html>
<head>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>instead do this
<html>
<head>
<style>
/* critical styles */
</style>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>Fetching non-critical styles
<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.
<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
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 --> AVIFCompress SVG's
Use AVIF for better performance
Fonts
@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
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
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
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
Box Model
Everything on the browser is a box.
flowchart TD
subgraph "Margin Box"
subgraph "Border Box"
subgraph "Padding Box"
subgraph "Content Box"
end
end
end
endBox 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-box5px border left + 20px padding left + 800px content + 20px padding right + 5px border right = 850px
border-box5px 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.
<p>Hi</p>
Iam a anonymous block elementBrowser 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.
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
endItems 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.
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
endThe 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
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]
endReflow 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.
// 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.
// optimsied reflow
@keyframes moving-down-fast {
from {
transform: translateY(0);
}
to {
transform: translateY(100px);
}
}Composition Layers
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>]:::orangeRender 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.
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]:::yellowDOM API
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: usageFor some weird reason, both HTMDocument and Element have access to the DOMAPI methods.
$0.querySelector;
document.querySelector;Global objects
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
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
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
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
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
// 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 > .targetComplex queries take longer to compute
Use Id's for core containers
CSS selectors are compiled by the browser on run time.
Inserting new elements
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.
<!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>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
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 onceWeb APIs for Complex UI Patterns
Observer APIs
Intersection Observers
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
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.
h1 = document.createElement('h1');
h1.textContent = textContent.slice(3);
target.replaceWith(h1);Resize Observers
.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)
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
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)
// 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
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 forHTTP 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?
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
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| ServerSplit 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%.
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
<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.
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| ServerInline critical styles
<html>
<head>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>instead do this
<html>
<head>
<style>
/* critical styles */
</style>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>Fetching non-critical styles
<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.
<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
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 --> AVIFCompress SVG's
Use AVIF for better performance
Fonts
@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
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
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
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
Box Model
No title
Everything on the browser is a box.
Rendering diagram...
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-box5px border left + 20px padding left + 800px content + 20px padding right + 5px border right = 850px
border-box5px 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.
<p>Hi</p>
Iam a anonymous block elementNo title
Everything on the browser is a box.
Rendering diagram...
Rendering diagram...
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-box5px border left + 20px padding left + 800px content + 20px padding right + 5px border right = 850px
border-box5px 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.
1<p>Hi</p>
2Iam a anonymous block elementblock
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.
anonymous
Text that is not wrapped in any tags are treated as anonymous block elements.
1<p>Hi</p>
2Iam a anonymous block element1<p>Hi</p>
2Iam a anonymous block elementBrowser 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.
Rendering diagram...
Items inside the flex formatting context are not affected by the outer (block formatting) context.
Rendering diagram...
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.
Rendering diagram...
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.
Rendering diagram...
Reflow
No title
Rendering diagram...
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.
1// non-optimised reflow
2@keyframes moving-down-slow {
3 from {
4 margin-top: 0;
5 }
6 to {
7 margin-top: 100px;
8 }
9}The below example is GPU intensive, since the layout does not need to be re-calculated. Only the element needs to be repainted.
// optimsied reflow
@keyframes moving-down-fast {
from {
transform: translateY(0);
}
to {
transform: translateY(100px);
}
}No title
Rendering diagram...
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.
Rendering diagram...
The below example is CPU intensive, since layout needs to be re-calculated.
1// non-optimised reflow
2@keyframes moving-down-slow {
3 from {
4 margin-top: 0;
5 }
6 to {
7 margin-top: 100px;
8 }
9}1// non-optimised reflow
2@keyframes moving-down-slow {
3 from {
4 margin-top: 0;
5 }
6 to {
7 margin-top: 100px;
8 }
9}The below example is GPU intensive, since the layout does not need to be re-calculated. Only the element needs to be repainted.
1// optimsied reflow
2@keyframes moving-down-fast {
3 from {
4 transform: translateY(0);
5 }
6 to {
7 transform: translateY(100px);
8 }
9}1// optimsied reflow
2@keyframes moving-down-fast {
3 from {
4 transform: translateY(0);
5 }
6 to {
7 transform: translateY(100px);
8 }
9}Composition Layers
No title
Rendering diagram...
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.
Rendering diagram...
No title
Rendering diagram...
Rendering diagram...
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.
Rendering diagram...
Rendering diagram...
DOM API
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: usageFor some weird reason, both HTMDocument and Element have access to the DOMAPI methods.
$0.querySelector;
document.querySelector;Global objects
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
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
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
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
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
// 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 > .targetComplex queries take longer to compute
Use Id's for core containers
CSS selectors are compiled by the browser on run time.
Inserting new elements
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.
<!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>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
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 onceNo title
Rendering diagram...
For some weird reason, both HTMDocument and Element have access to the DOMAPI methods.
1$0.querySelector;
2document.querySelector;Rendering diagram...
1$0.querySelector;
2document.querySelector;Global objects
1console.log(typeof window); // object
2console.log(window.document === document); // true
3console.log(window.document.body); // <body></body>
4console.log(window.document.head); // <head></head>1console.log(typeof window); // object
2console.log(window.document === document); // true
3console.log(window.document.body); // <body></body>
4console.log(window.document.head); // <head></head>DOM Querying
getElementById
1document.getElementById('id'); // O(1)O(1) time complexity
Id namespace is shared, across all components.
Make suer we dont overuse ids.
getElementsByTagName or getElementsByClassName
1document.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
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
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.
getElementById
1document.getElementById('id'); // O(1)O(1) time complexity
Id namespace is shared, across all components.
Make suer we dont overuse ids.
1document.getElementById('id'); // O(1)getElementsByTagName or getElementsByClassName
1document.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.
1document.getElementsByClassName('class'); // O(N)querySelector
1document.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.
1document.querySelector('.class'); // O(N)querySelectorAll
1document.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.
1document.querySelectorAll('.class'); // O(N)Performance optimisations
CSS Selectors
1// this is slow, since we need traverse longer
2div > div > section > .flex > .target
3
4// this is faster (When # is used we can skip the DOM tree traversal, upto the id)
5#container.flex > .targetComplex queries take longer to compute
Use Id's for core containers
CSS selectors are compiled by the browser on run time.
Inserting new elements
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.
<!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>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
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 onceCSS Selectors
1// this is slow, since we need traverse longer
2div > div > section > .flex > .target
3
4// this is faster (When # is used we can skip the DOM tree traversal, upto the id)
5#container.flex > .targetComplex queries take longer to compute
Use Id's for core containers
CSS selectors are compiled by the browser on run time.
1// this is slow, since we need traverse longer
2div > div > section > .flex > .target
3
4// this is faster (When # is used we can skip the DOM tree traversal, upto the id)
5#container.flex > .targetInserting new elements
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.
<!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>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');1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title></title>
7 <link href="./index.css" rel="stylesheet" />
8 </head>
9 <body>
10 <template id="template">
11 <div class="card">
12 <h3 class="card-title"></h3>
13 <div class="card-content">
14 <p class="card-desc"></p>
15 <button>Click me</button>
16 </div>
17 </div>
18 </template>
19 </body>
20 <script src="./index.js"></script>
21</html>1const template = document.getElementById('template');
2
3const createAndAppendCard = (title, desc) => {
4 const card = template.content.cloneNode(true).firstElementChild;
5 // cloneNod(isDeepClone?) deep clone to avoid nested elements from being modified by other clones
6 // template.content.cloneNode(true) returns a DocumentFragment
7 // firstElementChild gets the first child of the template / documentFragment
8 const titleEl = card.querySelector('.card-title');
9 const descEl = card.querySelector('.card-desc');
10
11 titleEl.innerText = title; // will cause reflow avoid
12 descEl.textContent = desc; // will not cause reflow
13
14 document.body.appendChild(card);
15 // O(N), causes re-flow
16};
17
18createAndAppendCard('Title 1', 'Description 1');DocumentFragment for dynamic templates
1const fragment = new DocumentFragment();
2for (let idx = 0; idx < 1000; idx++) {
3 const card = template.content.cloneNode(true).firstElementChild;
4 const titleEl = card.querySelector('.card-title');
5 const descEl = card.querySelector('.card-desc');
6
7 titleEl.innerText = `Title ${idx}`;
8 descEl.textContent = `Description ${idx}`;
9
10 fragment.appendChild(card);
11}
12document.body.appendChild(fragment); // this operation causes re-flow only once1const fragment = new DocumentFragment();
2for (let idx = 0; idx < 1000; idx++) {
3 const card = template.content.cloneNode(true).firstElementChild;
4 const titleEl = card.querySelector('.card-title');
5 const descEl = card.querySelector('.card-desc');
6
7 titleEl.innerText = `Title ${idx}`;
8 descEl.textContent = `Description ${idx}`;
9
10 fragment.appendChild(card);
11}
12document.body.appendChild(fragment); // this operation causes re-flow only onceWeb APIs for Complex UI Patterns
Observer APIs
Intersection Observers
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
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.
h1 = document.createElement('h1');
h1.textContent = textContent.slice(3);
target.replaceWith(h1);Resize Observers
No title
.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)
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);Observer APIs
Intersection Observers
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
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.
h1 = document.createElement('h1');
h1.textContent = textContent.slice(3);
target.replaceWith(h1);Resize Observers
No title
.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)
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);Intersection Observers
1const callback = (entries, observer) => {
2 entries.forEach(entry => {
3 if (entry.isIntersecting) {
4 console.log('Element is in view');
5 } else {
6 console.log('Element is out of view');
7 }
8 });
9};
10
11const observer = new IntersectionObserver(callback, {
12 threshold: 0.5,
13 root: document.querySelector('.container'),
14});
15
16observer.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.
1const callback = (entries, observer) => {
2 entries.forEach(entry => {
3 if (entry.isIntersecting) {
4 console.log('Element is in view');
5 } else {
6 console.log('Element is out of view');
7 }
8 });
9};
10
11const observer = new IntersectionObserver(callback, {
12 threshold: 0.5,
13 root: document.querySelector('.container'),
14});
15
16observer.observe(document.querySelector('.target'));Mutation Observers
Drawing tools
Rich text editors
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.
1h1 = document.createElement('h1');
2h1.textContent = textContent.slice(3);
3target.replaceWith(h1);1type MutationRecord = {
2 type: 'childList' | 'attributes' | 'subtree' | 'characterData';
3 target: Node; // the node that was changed
4 addedNodes: NodeList; // nodes added with notation
5 removedNodes: NodeList; // nodes removed with notation
6 oldValue: string; // prev value
7};
8
9const callback = ((mutationsList) => {
10 mutationsList.forEach(mutation: MutationRecord) => {
11 // do something with the mutation
12 });
13};
14
15let observer = new MutationObserver(callback);
16observer.observe(document.querySelector('.target')!, {
17 childList: true, // tracks only immediate children, has higher priority over subTree
18 attributes: true, // track attribute changes
19 subtree: true, // track changes in descendants
20 characterData: true, // tracks text changes
21 attributesFilter: ['class', 'id'],
22});1h1 = document.createElement('h1');
2h1.textContent = textContent.slice(3);
3target.replaceWith(h1);Resize Observers
No title
1.blog-section {
2 width: 100%; // width of the container should not be auto
3 margin-bottom: $margin-bottom--6;
4 container: blog-section-container;
5 container-type: inline-size;
6
7 &__title {
8 margin-bottom: $margin-bottom--2;
9 }
10}
11
12@container blog-section-container (max-width: 400px) {
13 .blog-section {
14 &__title {
15 color: red;
16 }
17 }
18}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)
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);No title
1.blog-section {
2 width: 100%; // width of the container should not be auto
3 margin-bottom: $margin-bottom--6;
4 container: blog-section-container;
5 container-type: inline-size;
6
7 &__title {
8 margin-bottom: $margin-bottom--2;
9 }
10}
11
12@container blog-section-container (max-width: 400px) {
13 .blog-section {
14 &__title {
15 color: red;
16 }
17 }
18}1.blog-section {
2 width: 100%; // width of the container should not be auto
3 margin-bottom: $margin-bottom--6;
4 container: blog-section-container;
5 container-type: inline-size;
6
7 &__title {
8 margin-bottom: $margin-bottom--2;
9 }
10}
11
12@container blog-section-container (max-width: 400px) {
13 .blog-section {
14 &__title {
15 color: red;
16 }
17 }
18}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)
1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.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
No title
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)
// 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
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 forHTTP 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?
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.
Application state design
No title
Rendering diagram...
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)
// 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
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 forHTTP 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?
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.
No title
Rendering diagram...
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)
Rendering diagram...
Data Normalization (1NF, 2NF, 3NF)
// 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
},
]1// 1NF,
2// all keys use a single atomic value
3const user_id_1 = {
4 id: 1, // primary key
5 name: 'John', // atomic
6 age: 30,
7 address_street: '123 Main St',
8 address_city: 'New York',
9 address_state: 'NY',
10 address_zip: '10001'
11};
12
13// 2 NF,
14const users = {
15 1: {
16 id: 1, // primary key
17 name: 'John', // name depends on id, primary key
18 age: 30,
19 address: {
20 street: '123 Main St',
21 // depends on address, address depends on id
22 city: 'New York',
23 state: 'NY',
24 zip: '10001'
25 }
26 },
27]
28
29// 3 NF, (usually excessive on the FE)
30const addresses: {
31 1: {
32 street: '123 Main St',
33 city: 'New York',
34 state: 'NY',
35 zip: '10001'
36 }
37};
38
39const users = {
40 1: {
41 id: 1,
42 name: 'John',
43 age: 30,
44 address: addressee[1] // no transitive dependency
45 },
46]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
No title
Rendering diagram...
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.
No title
Rendering diagram...
HTTP 1.1 doesn't compress headers.
HTTP 2 compress headers,
Rendering diagram...
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.
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?
Rendering diagram...
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?
Rendering diagram...
Rendering diagram...
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
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| ServerSplit 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%.
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
<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.
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| ServerInline critical styles
<html>
<head>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>instead do this
<html>
<head>
<style>
/* critical styles */
</style>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>Fetching non-critical styles
<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.
<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
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 --> AVIFCompress SVG's
Use AVIF for better performance
Fonts
@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
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
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"]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
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| ServerSplit 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%.
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
<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.
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| ServerInline critical styles
<html>
<head>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>instead do this
<html>
<head>
<style>
/* critical styles */
</style>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>Fetching non-critical styles
<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.
<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
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 --> AVIFCompress SVG's
Use AVIF for better performance
Fonts
@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.
LCP, (Largest Contentful Paint)
Loading performace of app
Should be < 2.5s
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
Rendering diagram...
Rendering diagram...
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%.
1function helloWold() {
2 const longVariableName = 5 + 5,
3 return longVariableName;
4}
5
6function h(){return 5+5}1function helloWold() {
2 const longVariableName = 5 + 5,
3 return longVariableName;
4}
5
6function h(){return 5+5}Change order of script loading
1<script src="module1.js"></script>
2<script src="anlaytics.js" defer></script>
3<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.
1<script src="module1.js"></script>
2<script src="anlaytics.js" defer></script>
3<script src="module1.js"></script>CSS can be minified and compressed as well.
No title
Rendering diagram...
Inline critical styles
<html>
<head>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>instead do this
<html>
<head>
<style>
/* critical styles */
</style>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>Fetching non-critical styles
<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.
<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
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 --> AVIFCompress SVG's
Use AVIF for better performance
Fonts
@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.
No title
Rendering diagram...
Rendering diagram...
Inline critical styles
1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>instead do this
1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>Fetching non-critical styles
1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</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.
1<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.
1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</html>1<link rel="preload" as="style" href="href://cdn.com/desktop.css" />Rendering diagram...
Fonts
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}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.
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}Summary
Rendering diagram...
JS performance improvements
1graph TD
2 Javascript
3 Javascript --> ReduceCPU["Reduce CPU Usage"]
4 Javascript --> ReduceRAM["Reduce RAM usage"]
5 Javascript --> DontBlockUI["Don't block UI thread"]
6 Javascript --> AsyncJobs["Utilize Async jobs"]
7
8 ReduceCPU --> MinimizeSearch["Minimize Search and access cost"]
9 ReduceRAM --> MinimizeRuntime["Minimize runtime state"]
10 MinimizeRuntime --> IndexedDB["Utilize IndexedDB"]
11
12 DontBlockUI --> AvoidRW["Avoid high frequency read-write to sync storages\nlocal storage or \nsession storage"]
13
14 AsyncJobs --> UseServer["Use server"]
15 AsyncJobs --> WebWorkers["Use Web Workers"]Rendering diagram...
1graph TD
2 Javascript
3 Javascript --> ReduceCPU["Reduce CPU Usage"]
4 Javascript --> ReduceRAM["Reduce RAM usage"]
5 Javascript --> DontBlockUI["Don't block UI thread"]
6 Javascript --> AsyncJobs["Utilize Async jobs"]
7
8 ReduceCPU --> MinimizeSearch["Minimize Search and access cost"]
9 ReduceRAM --> MinimizeRuntime["Minimize runtime state"]
10 MinimizeRuntime --> IndexedDB["Utilize IndexedDB"]
11
12 DontBlockUI --> AvoidRW["Avoid high frequency read-write to sync storages\nlocal storage or \nsession storage"]
13
14 AsyncJobs --> UseServer["Use server"]
15 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
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
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
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
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.
API
Data model
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
Data model
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',
},
},
};1const stories = {
2 story_id: {
3 text: 'Story text',
4 story_id: 'story_id',
5 timestamp: 'timestamp',
6 },
7};
8
9const comments = {
10 story_id: {
11 comment_id: {
12 text: 'Comment text',
13 id: 'id',
14 timestamp: 'timestamp',
15 },
16 },
17};
18
19// 2nf, 3nf looks excessive
20const attachments = {
21 story_id: {
22 attachment_id: {
23 url: 'url',
24 type: 'image',
25 timestamp: 'timestamp',
26 },
27 },
28};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
Short polling / Long polling
Battery drain
Data inefficient
Latency (TCP re-connection)
Simple
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
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
Box Model
No title
Everything on the browser is a box.
Rendering diagram...
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-box5px border left + 20px padding left + 800px content + 20px padding right + 5px border right = 850px
border-box5px 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.
<p>Hi</p>
Iam a anonymous block elementNo title
Everything on the browser is a box.
Rendering diagram...
Rendering diagram...
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-box5px border left + 20px padding left + 800px content + 20px padding right + 5px border right = 850px
border-box5px 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.
1<p>Hi</p>
2Iam a anonymous block elementblock
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.
anonymous
Text that is not wrapped in any tags are treated as anonymous block elements.
1<p>Hi</p>
2Iam a anonymous block element1<p>Hi</p>
2Iam a anonymous block elementBrowser 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.
Rendering diagram...
Items inside the flex formatting context are not affected by the outer (block formatting) context.
Rendering diagram...
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.
Rendering diagram...
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.
Rendering diagram...
Reflow
No title
Rendering diagram...
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.
1// non-optimised reflow
2@keyframes moving-down-slow {
3 from {
4 margin-top: 0;
5 }
6 to {
7 margin-top: 100px;
8 }
9}The below example is GPU intensive, since the layout does not need to be re-calculated. Only the element needs to be repainted.
// optimsied reflow
@keyframes moving-down-fast {
from {
transform: translateY(0);
}
to {
transform: translateY(100px);
}
}No title
Rendering diagram...
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.
Rendering diagram...
The below example is CPU intensive, since layout needs to be re-calculated.
1// non-optimised reflow
2@keyframes moving-down-slow {
3 from {
4 margin-top: 0;
5 }
6 to {
7 margin-top: 100px;
8 }
9}1// non-optimised reflow
2@keyframes moving-down-slow {
3 from {
4 margin-top: 0;
5 }
6 to {
7 margin-top: 100px;
8 }
9}The below example is GPU intensive, since the layout does not need to be re-calculated. Only the element needs to be repainted.
1// optimsied reflow
2@keyframes moving-down-fast {
3 from {
4 transform: translateY(0);
5 }
6 to {
7 transform: translateY(100px);
8 }
9}1// optimsied reflow
2@keyframes moving-down-fast {
3 from {
4 transform: translateY(0);
5 }
6 to {
7 transform: translateY(100px);
8 }
9}Composition Layers
No title
Rendering diagram...
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.
Rendering diagram...
No title
Rendering diagram...
Rendering diagram...
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.
Rendering diagram...
Rendering diagram...
DOM API
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: usageFor some weird reason, both HTMDocument and Element have access to the DOMAPI methods.
$0.querySelector;
document.querySelector;Global objects
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
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
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
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
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
// 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 > .targetComplex queries take longer to compute
Use Id's for core containers
CSS selectors are compiled by the browser on run time.
Inserting new elements
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.
<!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>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
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 onceNo title
Rendering diagram...
For some weird reason, both HTMDocument and Element have access to the DOMAPI methods.
1$0.querySelector;
2document.querySelector;Rendering diagram...
1$0.querySelector;
2document.querySelector;Global objects
1console.log(typeof window); // object
2console.log(window.document === document); // true
3console.log(window.document.body); // <body></body>
4console.log(window.document.head); // <head></head>1console.log(typeof window); // object
2console.log(window.document === document); // true
3console.log(window.document.body); // <body></body>
4console.log(window.document.head); // <head></head>DOM Querying
getElementById
1document.getElementById('id'); // O(1)O(1) time complexity
Id namespace is shared, across all components.
Make suer we dont overuse ids.
getElementsByTagName or getElementsByClassName
1document.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
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
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.
getElementById
1document.getElementById('id'); // O(1)O(1) time complexity
Id namespace is shared, across all components.
Make suer we dont overuse ids.
1document.getElementById('id'); // O(1)getElementsByTagName or getElementsByClassName
1document.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.
1document.getElementsByClassName('class'); // O(N)querySelector
1document.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.
1document.querySelector('.class'); // O(N)querySelectorAll
1document.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.
1document.querySelectorAll('.class'); // O(N)Performance optimisations
CSS Selectors
1// this is slow, since we need traverse longer
2div > div > section > .flex > .target
3
4// this is faster (When # is used we can skip the DOM tree traversal, upto the id)
5#container.flex > .targetComplex queries take longer to compute
Use Id's for core containers
CSS selectors are compiled by the browser on run time.
Inserting new elements
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.
<!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>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
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 onceCSS Selectors
1// this is slow, since we need traverse longer
2div > div > section > .flex > .target
3
4// this is faster (When # is used we can skip the DOM tree traversal, upto the id)
5#container.flex > .targetComplex queries take longer to compute
Use Id's for core containers
CSS selectors are compiled by the browser on run time.
1// this is slow, since we need traverse longer
2div > div > section > .flex > .target
3
4// this is faster (When # is used we can skip the DOM tree traversal, upto the id)
5#container.flex > .targetInserting new elements
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.
<!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>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');1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title></title>
7 <link href="./index.css" rel="stylesheet" />
8 </head>
9 <body>
10 <template id="template">
11 <div class="card">
12 <h3 class="card-title"></h3>
13 <div class="card-content">
14 <p class="card-desc"></p>
15 <button>Click me</button>
16 </div>
17 </div>
18 </template>
19 </body>
20 <script src="./index.js"></script>
21</html>1const template = document.getElementById('template');
2
3const createAndAppendCard = (title, desc) => {
4 const card = template.content.cloneNode(true).firstElementChild;
5 // cloneNod(isDeepClone?) deep clone to avoid nested elements from being modified by other clones
6 // template.content.cloneNode(true) returns a DocumentFragment
7 // firstElementChild gets the first child of the template / documentFragment
8 const titleEl = card.querySelector('.card-title');
9 const descEl = card.querySelector('.card-desc');
10
11 titleEl.innerText = title; // will cause reflow avoid
12 descEl.textContent = desc; // will not cause reflow
13
14 document.body.appendChild(card);
15 // O(N), causes re-flow
16};
17
18createAndAppendCard('Title 1', 'Description 1');DocumentFragment for dynamic templates
1const fragment = new DocumentFragment();
2for (let idx = 0; idx < 1000; idx++) {
3 const card = template.content.cloneNode(true).firstElementChild;
4 const titleEl = card.querySelector('.card-title');
5 const descEl = card.querySelector('.card-desc');
6
7 titleEl.innerText = `Title ${idx}`;
8 descEl.textContent = `Description ${idx}`;
9
10 fragment.appendChild(card);
11}
12document.body.appendChild(fragment); // this operation causes re-flow only once1const fragment = new DocumentFragment();
2for (let idx = 0; idx < 1000; idx++) {
3 const card = template.content.cloneNode(true).firstElementChild;
4 const titleEl = card.querySelector('.card-title');
5 const descEl = card.querySelector('.card-desc');
6
7 titleEl.innerText = `Title ${idx}`;
8 descEl.textContent = `Description ${idx}`;
9
10 fragment.appendChild(card);
11}
12document.body.appendChild(fragment); // this operation causes re-flow only onceWeb APIs for Complex UI Patterns
Observer APIs
Intersection Observers
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
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.
h1 = document.createElement('h1');
h1.textContent = textContent.slice(3);
target.replaceWith(h1);Resize Observers
No title
.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)
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);Observer APIs
Intersection Observers
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
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.
h1 = document.createElement('h1');
h1.textContent = textContent.slice(3);
target.replaceWith(h1);Resize Observers
No title
.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)
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);Intersection Observers
1const callback = (entries, observer) => {
2 entries.forEach(entry => {
3 if (entry.isIntersecting) {
4 console.log('Element is in view');
5 } else {
6 console.log('Element is out of view');
7 }
8 });
9};
10
11const observer = new IntersectionObserver(callback, {
12 threshold: 0.5,
13 root: document.querySelector('.container'),
14});
15
16observer.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.
1const callback = (entries, observer) => {
2 entries.forEach(entry => {
3 if (entry.isIntersecting) {
4 console.log('Element is in view');
5 } else {
6 console.log('Element is out of view');
7 }
8 });
9};
10
11const observer = new IntersectionObserver(callback, {
12 threshold: 0.5,
13 root: document.querySelector('.container'),
14});
15
16observer.observe(document.querySelector('.target'));Mutation Observers
Drawing tools
Rich text editors
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.
1h1 = document.createElement('h1');
2h1.textContent = textContent.slice(3);
3target.replaceWith(h1);1type MutationRecord = {
2 type: 'childList' | 'attributes' | 'subtree' | 'characterData';
3 target: Node; // the node that was changed
4 addedNodes: NodeList; // nodes added with notation
5 removedNodes: NodeList; // nodes removed with notation
6 oldValue: string; // prev value
7};
8
9const callback = ((mutationsList) => {
10 mutationsList.forEach(mutation: MutationRecord) => {
11 // do something with the mutation
12 });
13};
14
15let observer = new MutationObserver(callback);
16observer.observe(document.querySelector('.target')!, {
17 childList: true, // tracks only immediate children, has higher priority over subTree
18 attributes: true, // track attribute changes
19 subtree: true, // track changes in descendants
20 characterData: true, // tracks text changes
21 attributesFilter: ['class', 'id'],
22});1h1 = document.createElement('h1');
2h1.textContent = textContent.slice(3);
3target.replaceWith(h1);Resize Observers
No title
1.blog-section {
2 width: 100%; // width of the container should not be auto
3 margin-bottom: $margin-bottom--6;
4 container: blog-section-container;
5 container-type: inline-size;
6
7 &__title {
8 margin-bottom: $margin-bottom--2;
9 }
10}
11
12@container blog-section-container (max-width: 400px) {
13 .blog-section {
14 &__title {
15 color: red;
16 }
17 }
18}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)
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);No title
1.blog-section {
2 width: 100%; // width of the container should not be auto
3 margin-bottom: $margin-bottom--6;
4 container: blog-section-container;
5 container-type: inline-size;
6
7 &__title {
8 margin-bottom: $margin-bottom--2;
9 }
10}
11
12@container blog-section-container (max-width: 400px) {
13 .blog-section {
14 &__title {
15 color: red;
16 }
17 }
18}1.blog-section {
2 width: 100%; // width of the container should not be auto
3 margin-bottom: $margin-bottom--6;
4 container: blog-section-container;
5 container-type: inline-size;
6
7 &__title {
8 margin-bottom: $margin-bottom--2;
9 }
10}
11
12@container blog-section-container (max-width: 400px) {
13 .blog-section {
14 &__title {
15 color: red;
16 }
17 }
18}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)
1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.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
No title
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)
// 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
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 forHTTP 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?
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.
Application state design
No title
Rendering diagram...
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)
// 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
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 forHTTP 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?
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.
No title
Rendering diagram...
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)
Rendering diagram...
Data Normalization (1NF, 2NF, 3NF)
// 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
},
]1// 1NF,
2// all keys use a single atomic value
3const user_id_1 = {
4 id: 1, // primary key
5 name: 'John', // atomic
6 age: 30,
7 address_street: '123 Main St',
8 address_city: 'New York',
9 address_state: 'NY',
10 address_zip: '10001'
11};
12
13// 2 NF,
14const users = {
15 1: {
16 id: 1, // primary key
17 name: 'John', // name depends on id, primary key
18 age: 30,
19 address: {
20 street: '123 Main St',
21 // depends on address, address depends on id
22 city: 'New York',
23 state: 'NY',
24 zip: '10001'
25 }
26 },
27]
28
29// 3 NF, (usually excessive on the FE)
30const addresses: {
31 1: {
32 street: '123 Main St',
33 city: 'New York',
34 state: 'NY',
35 zip: '10001'
36 }
37};
38
39const users = {
40 1: {
41 id: 1,
42 name: 'John',
43 age: 30,
44 address: addressee[1] // no transitive dependency
45 },
46]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
No title
Rendering diagram...
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.
No title
Rendering diagram...
HTTP 1.1 doesn't compress headers.
HTTP 2 compress headers,
Rendering diagram...
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.
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?
Rendering diagram...
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?
Rendering diagram...
Rendering diagram...
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
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| ServerSplit 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%.
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
<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.
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| ServerInline critical styles
<html>
<head>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>instead do this
<html>
<head>
<style>
/* critical styles */
</style>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>Fetching non-critical styles
<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.
<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
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 --> AVIFCompress SVG's
Use AVIF for better performance
Fonts
@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
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
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"]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
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| ServerSplit 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%.
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
<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.
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| ServerInline critical styles
<html>
<head>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>instead do this
<html>
<head>
<style>
/* critical styles */
</style>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>Fetching non-critical styles
<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.
<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
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 --> AVIFCompress SVG's
Use AVIF for better performance
Fonts
@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.
LCP, (Largest Contentful Paint)
Loading performace of app
Should be < 2.5s
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
Rendering diagram...
Rendering diagram...
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%.
1function helloWold() {
2 const longVariableName = 5 + 5,
3 return longVariableName;
4}
5
6function h(){return 5+5}1function helloWold() {
2 const longVariableName = 5 + 5,
3 return longVariableName;
4}
5
6function h(){return 5+5}Change order of script loading
1<script src="module1.js"></script>
2<script src="anlaytics.js" defer></script>
3<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.
1<script src="module1.js"></script>
2<script src="anlaytics.js" defer></script>
3<script src="module1.js"></script>CSS can be minified and compressed as well.
No title
Rendering diagram...
Inline critical styles
<html>
<head>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>instead do this
<html>
<head>
<style>
/* critical styles */
</style>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>Fetching non-critical styles
<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.
<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
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 --> AVIFCompress SVG's
Use AVIF for better performance
Fonts
@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.
No title
Rendering diagram...
Rendering diagram...
Inline critical styles
1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>instead do this
1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>Fetching non-critical styles
1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</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.
1<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.
1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</html>1<link rel="preload" as="style" href="href://cdn.com/desktop.css" />Rendering diagram...
Fonts
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}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.
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}Summary
Rendering diagram...
JS performance improvements
1graph TD
2 Javascript
3 Javascript --> ReduceCPU["Reduce CPU Usage"]
4 Javascript --> ReduceRAM["Reduce RAM usage"]
5 Javascript --> DontBlockUI["Don't block UI thread"]
6 Javascript --> AsyncJobs["Utilize Async jobs"]
7
8 ReduceCPU --> MinimizeSearch["Minimize Search and access cost"]
9 ReduceRAM --> MinimizeRuntime["Minimize runtime state"]
10 MinimizeRuntime --> IndexedDB["Utilize IndexedDB"]
11
12 DontBlockUI --> AvoidRW["Avoid high frequency read-write to sync storages\nlocal storage or \nsession storage"]
13
14 AsyncJobs --> UseServer["Use server"]
15 AsyncJobs --> WebWorkers["Use Web Workers"]Rendering diagram...
1graph TD
2 Javascript
3 Javascript --> ReduceCPU["Reduce CPU Usage"]
4 Javascript --> ReduceRAM["Reduce RAM usage"]
5 Javascript --> DontBlockUI["Don't block UI thread"]
6 Javascript --> AsyncJobs["Utilize Async jobs"]
7
8 ReduceCPU --> MinimizeSearch["Minimize Search and access cost"]
9 ReduceRAM --> MinimizeRuntime["Minimize runtime state"]
10 MinimizeRuntime --> IndexedDB["Utilize IndexedDB"]
11
12 DontBlockUI --> AvoidRW["Avoid high frequency read-write to sync storages\nlocal storage or \nsession storage"]
13
14 AsyncJobs --> UseServer["Use server"]
15 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
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
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
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
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.
API
Data model
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
Data model
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',
},
},
};1const stories = {
2 story_id: {
3 text: 'Story text',
4 story_id: 'story_id',
5 timestamp: 'timestamp',
6 },
7};
8
9const comments = {
10 story_id: {
11 comment_id: {
12 text: 'Comment text',
13 id: 'id',
14 timestamp: 'timestamp',
15 },
16 },
17};
18
19// 2nf, 3nf looks excessive
20const attachments = {
21 story_id: {
22 attachment_id: {
23 url: 'url',
24 type: 'image',
25 timestamp: 'timestamp',
26 },
27 },
28};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
Short polling / Long polling
Battery drain
Data inefficient
Latency (TCP re-connection)
Simple
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
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
1<p>Hi</p>
2Iam a anonymous block elementThe below example is GPU intensive, since the layout does not need to be re-calculated. Only the element needs to be repainted.
1// optimsied reflow
2@keyframes moving-down-fast {
3 from {
4 transform: translateY(0);
5 }
6 to {
7 transform: translateY(100px);
8 }
9}1// optimsied reflow
2@keyframes moving-down-fast {
3 from {
4 transform: translateY(0);
5 }
6 to {
7 transform: translateY(100px);
8 }
9}No title
Rendering diagram...
For some weird reason, both HTMDocument and Element have access to the DOMAPI methods.
1$0.querySelector;
2document.querySelector;Rendering diagram...
1$0.querySelector;
2document.querySelector;Global objects
1console.log(typeof window); // object
2console.log(window.document === document); // true
3console.log(window.document.body); // <body></body>
4console.log(window.document.head); // <head></head>1console.log(typeof window); // object
2console.log(window.document === document); // true
3console.log(window.document.body); // <body></body>
4console.log(window.document.head); // <head></head>DOM Querying
getElementById
1document.getElementById('id'); // O(1)O(1) time complexity
Id namespace is shared, across all components.
Make suer we dont overuse ids.
getElementsByTagName or getElementsByClassName
1document.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
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
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.
getElementById
1document.getElementById('id'); // O(1)O(1) time complexity
Id namespace is shared, across all components.
Make suer we dont overuse ids.
1document.getElementById('id'); // O(1)getElementsByTagName or getElementsByClassName
1document.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.
1document.getElementsByClassName('class'); // O(N)querySelector
1document.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.
1document.querySelector('.class'); // O(N)querySelectorAll
1document.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.
1document.querySelectorAll('.class'); // O(N)Performance optimisations
CSS Selectors
1// this is slow, since we need traverse longer
2div > div > section > .flex > .target
3
4// this is faster (When # is used we can skip the DOM tree traversal, upto the id)
5#container.flex > .targetComplex queries take longer to compute
Use Id's for core containers
CSS selectors are compiled by the browser on run time.
Inserting new elements
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.
<!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>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
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 onceCSS Selectors
1// this is slow, since we need traverse longer
2div > div > section > .flex > .target
3
4// this is faster (When # is used we can skip the DOM tree traversal, upto the id)
5#container.flex > .targetComplex queries take longer to compute
Use Id's for core containers
CSS selectors are compiled by the browser on run time.
1// this is slow, since we need traverse longer
2div > div > section > .flex > .target
3
4// this is faster (When # is used we can skip the DOM tree traversal, upto the id)
5#container.flex > .targetInserting new elements
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.
<!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>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');1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title></title>
7 <link href="./index.css" rel="stylesheet" />
8 </head>
9 <body>
10 <template id="template">
11 <div class="card">
12 <h3 class="card-title"></h3>
13 <div class="card-content">
14 <p class="card-desc"></p>
15 <button>Click me</button>
16 </div>
17 </div>
18 </template>
19 </body>
20 <script src="./index.js"></script>
21</html>1const template = document.getElementById('template');
2
3const createAndAppendCard = (title, desc) => {
4 const card = template.content.cloneNode(true).firstElementChild;
5 // cloneNod(isDeepClone?) deep clone to avoid nested elements from being modified by other clones
6 // template.content.cloneNode(true) returns a DocumentFragment
7 // firstElementChild gets the first child of the template / documentFragment
8 const titleEl = card.querySelector('.card-title');
9 const descEl = card.querySelector('.card-desc');
10
11 titleEl.innerText = title; // will cause reflow avoid
12 descEl.textContent = desc; // will not cause reflow
13
14 document.body.appendChild(card);
15 // O(N), causes re-flow
16};
17
18createAndAppendCard('Title 1', 'Description 1');DocumentFragment for dynamic templates
1const fragment = new DocumentFragment();
2for (let idx = 0; idx < 1000; idx++) {
3 const card = template.content.cloneNode(true).firstElementChild;
4 const titleEl = card.querySelector('.card-title');
5 const descEl = card.querySelector('.card-desc');
6
7 titleEl.innerText = `Title ${idx}`;
8 descEl.textContent = `Description ${idx}`;
9
10 fragment.appendChild(card);
11}
12document.body.appendChild(fragment); // this operation causes re-flow only once1const fragment = new DocumentFragment();
2for (let idx = 0; idx < 1000; idx++) {
3 const card = template.content.cloneNode(true).firstElementChild;
4 const titleEl = card.querySelector('.card-title');
5 const descEl = card.querySelector('.card-desc');
6
7 titleEl.innerText = `Title ${idx}`;
8 descEl.textContent = `Description ${idx}`;
9
10 fragment.appendChild(card);
11}
12document.body.appendChild(fragment); // this operation causes re-flow only once1document.querySelector('.class'); // O(N)querySelectorAll
1document.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.
1document.querySelectorAll('.class'); // O(N)Inserting new elements
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.
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.
<!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>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');1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title></title>
7 <link href="./index.css" rel="stylesheet" />
8 </head>
9 <body>
10 <template id="template">
11 <div class="card">
12 <h3 class="card-title"></h3>
13 <div class="card-content">
14 <p class="card-desc"></p>
15 <button>Click me</button>
16 </div>
17 </div>
18 </template>
19 </body>
20 <script src="./index.js"></script>
21</html>1const template = document.getElementById('template');
2
3const createAndAppendCard = (title, desc) => {
4 const card = template.content.cloneNode(true).firstElementChild;
5 // cloneNod(isDeepClone?) deep clone to avoid nested elements from being modified by other clones
6 // template.content.cloneNode(true) returns a DocumentFragment
7 // firstElementChild gets the first child of the template / documentFragment
8 const titleEl = card.querySelector('.card-title');
9 const descEl = card.querySelector('.card-desc');
10
11 titleEl.innerText = title; // will cause reflow avoid
12 descEl.textContent = desc; // will not cause reflow
13
14 document.body.appendChild(card);
15 // O(N), causes re-flow
16};
17
18createAndAppendCard('Title 1', 'Description 1');DocumentFragment for dynamic templates
1const fragment = new DocumentFragment();
2for (let idx = 0; idx < 1000; idx++) {
3 const card = template.content.cloneNode(true).firstElementChild;
4 const titleEl = card.querySelector('.card-title');
5 const descEl = card.querySelector('.card-desc');
6
7 titleEl.innerText = `Title ${idx}`;
8 descEl.textContent = `Description ${idx}`;
9
10 fragment.appendChild(card);
11}
12document.body.appendChild(fragment); // this operation causes re-flow only once1const fragment = new DocumentFragment();
2for (let idx = 0; idx < 1000; idx++) {
3 const card = template.content.cloneNode(true).firstElementChild;
4 const titleEl = card.querySelector('.card-title');
5 const descEl = card.querySelector('.card-desc');
6
7 titleEl.innerText = `Title ${idx}`;
8 descEl.textContent = `Description ${idx}`;
9
10 fragment.appendChild(card);
11}
12document.body.appendChild(fragment); // this operation causes re-flow only once1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title></title>
7 <link href="./index.css" rel="stylesheet" />
8 </head>
9 <body>
10 <template id="template">
11 <div class="card">
12 <h3 class="card-title"></h3>
13 <div class="card-content">
14 <p class="card-desc"></p>
15 <button>Click me</button>
16 </div>
17 </div>
18 </template>
19 </body>
20 <script src="./index.js"></script>
21</html>1const template = document.getElementById('template');
2
3const createAndAppendCard = (title, desc) => {
4 const card = template.content.cloneNode(true).firstElementChild;
5 // cloneNod(isDeepClone?) deep clone to avoid nested elements from being modified by other clones
6 // template.content.cloneNode(true) returns a DocumentFragment
7 // firstElementChild gets the first child of the template / documentFragment
8 const titleEl = card.querySelector('.card-title');
9 const descEl = card.querySelector('.card-desc');
10
11 titleEl.innerText = title; // will cause reflow avoid
12 descEl.textContent = desc; // will not cause reflow
13
14 document.body.appendChild(card);
15 // O(N), causes re-flow
16};
17
18createAndAppendCard('Title 1', 'Description 1');Observer APIs
Intersection Observers
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
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.
h1 = document.createElement('h1');
h1.textContent = textContent.slice(3);
target.replaceWith(h1);Resize Observers
No title
.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)
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);1const callback = (entries, observer) => {
2 entries.forEach(entry => {
3 if (entry.isIntersecting) {
4 console.log('Element is in view');
5 } else {
6 console.log('Element is out of view');
7 }
8 });
9};
10
11const observer = new IntersectionObserver(callback, {
12 threshold: 0.5,
13 root: document.querySelector('.container'),
14});
15
16observer.observe(document.querySelector('.target'));1type MutationRecord = {
2 type: 'childList' | 'attributes' | 'subtree' | 'characterData';
3 target: Node; // the node that was changed
4 addedNodes: NodeList; // nodes added with notation
5 removedNodes: NodeList; // nodes removed with notation
6 oldValue: string; // prev value
7};
8
9const callback = ((mutationsList) => {
10 mutationsList.forEach(mutation: MutationRecord) => {
11 // do something with the mutation
12 });
13};
14
15let observer = new MutationObserver(callback);
16observer.observe(document.querySelector('.target')!, {
17 childList: true, // tracks only immediate children, has higher priority over subTree
18 attributes: true, // track attribute changes
19 subtree: true, // track changes in descendants
20 characterData: true, // tracks text changes
21 attributesFilter: ['class', 'id'],
22});1h1 = document.createElement('h1');
2h1.textContent = textContent.slice(3);
3target.replaceWith(h1);Resize Observers
No title
1.blog-section {
2 width: 100%; // width of the container should not be auto
3 margin-bottom: $margin-bottom--6;
4 container: blog-section-container;
5 container-type: inline-size;
6
7 &__title {
8 margin-bottom: $margin-bottom--2;
9 }
10}
11
12@container blog-section-container (max-width: 400px) {
13 .blog-section {
14 &__title {
15 color: red;
16 }
17 }
18}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)
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);1.blog-section {
2 width: 100%; // width of the container should not be auto
3 margin-bottom: $margin-bottom--6;
4 container: blog-section-container;
5 container-type: inline-size;
6
7 &__title {
8 margin-bottom: $margin-bottom--2;
9 }
10}
11
12@container blog-section-container (max-width: 400px) {
13 .blog-section {
14 &__title {
15 color: red;
16 }
17 }
18}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)
1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1const callback = (entries, observer) => {
2 entries.forEach(entry => {
3 if (entry.isIntersecting) {
4 console.log('Element is in view');
5 } else {
6 console.log('Element is out of view');
7 }
8 });
9};
10
11const observer = new IntersectionObserver(callback, {
12 threshold: 0.5,
13 root: document.querySelector('.container'),
14});
15
16observer.observe(document.querySelector('.target'));1type MutationRecord = {
2 type: 'childList' | 'attributes' | 'subtree' | 'characterData';
3 target: Node; // the node that was changed
4 addedNodes: NodeList; // nodes added with notation
5 removedNodes: NodeList; // nodes removed with notation
6 oldValue: string; // prev value
7};
8
9const callback = ((mutationsList) => {
10 mutationsList.forEach(mutation: MutationRecord) => {
11 // do something with the mutation
12 });
13};
14
15let observer = new MutationObserver(callback);
16observer.observe(document.querySelector('.target')!, {
17 childList: true, // tracks only immediate children, has higher priority over subTree
18 attributes: true, // track attribute changes
19 subtree: true, // track changes in descendants
20 characterData: true, // tracks text changes
21 attributesFilter: ['class', 'id'],
22});1h1 = document.createElement('h1');
2h1.textContent = textContent.slice(3);
3target.replaceWith(h1);Resize Observers
No title
1.blog-section {
2 width: 100%; // width of the container should not be auto
3 margin-bottom: $margin-bottom--6;
4 container: blog-section-container;
5 container-type: inline-size;
6
7 &__title {
8 margin-bottom: $margin-bottom--2;
9 }
10}
11
12@container blog-section-container (max-width: 400px) {
13 .blog-section {
14 &__title {
15 color: red;
16 }
17 }
18}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)
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);1.blog-section {
2 width: 100%; // width of the container should not be auto
3 margin-bottom: $margin-bottom--6;
4 container: blog-section-container;
5 container-type: inline-size;
6
7 &__title {
8 margin-bottom: $margin-bottom--2;
9 }
10}
11
12@container blog-section-container (max-width: 400px) {
13 .blog-section {
14 &__title {
15 color: red;
16 }
17 }
18}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)
1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1type MutationRecord = {
2 type: 'childList' | 'attributes' | 'subtree' | 'characterData';
3 target: Node; // the node that was changed
4 addedNodes: NodeList; // nodes added with notation
5 removedNodes: NodeList; // nodes removed with notation
6 oldValue: string; // prev value
7};
8
9const callback = ((mutationsList) => {
10 mutationsList.forEach(mutation: MutationRecord) => {
11 // do something with the mutation
12 });
13};
14
15let observer = new MutationObserver(callback);
16observer.observe(document.querySelector('.target')!, {
17 childList: true, // tracks only immediate children, has higher priority over subTree
18 attributes: true, // track attribute changes
19 subtree: true, // track changes in descendants
20 characterData: true, // tracks text changes
21 attributesFilter: ['class', 'id'],
22});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)
1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);Application state design
No title
Rendering diagram...
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)
// 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
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 forHTTP 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?
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.
Rendering diagram...
Data Normalization (1NF, 2NF, 3NF)
// 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
},
]1// 1NF,
2// all keys use a single atomic value
3const user_id_1 = {
4 id: 1, // primary key
5 name: 'John', // atomic
6 age: 30,
7 address_street: '123 Main St',
8 address_city: 'New York',
9 address_state: 'NY',
10 address_zip: '10001'
11};
12
13// 2 NF,
14const users = {
15 1: {
16 id: 1, // primary key
17 name: 'John', // name depends on id, primary key
18 age: 30,
19 address: {
20 street: '123 Main St',
21 // depends on address, address depends on id
22 city: 'New York',
23 state: 'NY',
24 zip: '10001'
25 }
26 },
27]
28
29// 3 NF, (usually excessive on the FE)
30const addresses: {
31 1: {
32 street: '123 Main St',
33 city: 'New York',
34 state: 'NY',
35 zip: '10001'
36 }
37};
38
39const users = {
40 1: {
41 id: 1,
42 name: 'John',
43 age: 30,
44 address: addressee[1] // no transitive dependency
45 },
46]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
No title
Rendering diagram...
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.
No title
Rendering diagram...
HTTP 1.1 doesn't compress headers.
HTTP 2 compress headers,
Rendering diagram...
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.
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?
Rendering diagram...
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?
Rendering diagram...
Rendering diagram...
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.
Data Normalization (1NF, 2NF, 3NF)
// 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
},
]1// 1NF,
2// all keys use a single atomic value
3const user_id_1 = {
4 id: 1, // primary key
5 name: 'John', // atomic
6 age: 30,
7 address_street: '123 Main St',
8 address_city: 'New York',
9 address_state: 'NY',
10 address_zip: '10001'
11};
12
13// 2 NF,
14const users = {
15 1: {
16 id: 1, // primary key
17 name: 'John', // name depends on id, primary key
18 age: 30,
19 address: {
20 street: '123 Main St',
21 // depends on address, address depends on id
22 city: 'New York',
23 state: 'NY',
24 zip: '10001'
25 }
26 },
27]
28
29// 3 NF, (usually excessive on the FE)
30const addresses: {
31 1: {
32 street: '123 Main St',
33 city: 'New York',
34 state: 'NY',
35 zip: '10001'
36 }
37};
38
39const users = {
40 1: {
41 id: 1,
42 name: 'John',
43 age: 30,
44 address: addressee[1] // no transitive dependency
45 },
46]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
No title
Rendering diagram...
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.
No title
Rendering diagram...
HTTP 1.1 doesn't compress headers.
HTTP 2 compress headers,
Rendering diagram...
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.
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?
Rendering diagram...
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?
Rendering diagram...
Rendering diagram...
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.
1// 1NF,
2// all keys use a single atomic value
3const user_id_1 = {
4 id: 1, // primary key
5 name: 'John', // atomic
6 age: 30,
7 address_street: '123 Main St',
8 address_city: 'New York',
9 address_state: 'NY',
10 address_zip: '10001'
11};
12
13// 2 NF,
14const users = {
15 1: {
16 id: 1, // primary key
17 name: 'John', // name depends on id, primary key
18 age: 30,
19 address: {
20 street: '123 Main St',
21 // depends on address, address depends on id
22 city: 'New York',
23 state: 'NY',
24 zip: '10001'
25 }
26 },
27]
28
29// 3 NF, (usually excessive on the FE)
30const addresses: {
31 1: {
32 street: '123 Main St',
33 city: 'New York',
34 state: 'NY',
35 zip: '10001'
36 }
37};
38
39const users = {
40 1: {
41 id: 1,
42 name: 'John',
43 age: 30,
44 address: addressee[1] // no transitive dependency
45 },
46]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
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| ServerSplit 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%.
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
<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.
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| ServerInline critical styles
<html>
<head>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>instead do this
<html>
<head>
<style>
/* critical styles */
</style>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>Fetching non-critical styles
<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.
<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
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 --> AVIFCompress SVG's
Use AVIF for better performance
Fonts
@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.
LCP, (Largest Contentful Paint)
Loading performace of app
Should be < 2.5s
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
Rendering diagram...
Rendering diagram...
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%.
1function helloWold() {
2 const longVariableName = 5 + 5,
3 return longVariableName;
4}
5
6function h(){return 5+5}1function helloWold() {
2 const longVariableName = 5 + 5,
3 return longVariableName;
4}
5
6function h(){return 5+5}Change order of script loading
1<script src="module1.js"></script>
2<script src="anlaytics.js" defer></script>
3<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.
1<script src="module1.js"></script>
2<script src="anlaytics.js" defer></script>
3<script src="module1.js"></script>CSS can be minified and compressed as well.
No title
Rendering diagram...
Inline critical styles
<html>
<head>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>instead do this
<html>
<head>
<style>
/* critical styles */
</style>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>Fetching non-critical styles
<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.
<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
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 --> AVIFCompress SVG's
Use AVIF for better performance
Fonts
@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.
No title
Rendering diagram...
Rendering diagram...
Inline critical styles
1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>instead do this
1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>Fetching non-critical styles
1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</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.
1<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.
1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</html>1<link rel="preload" as="style" href="href://cdn.com/desktop.css" />Rendering diagram...
Fonts
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}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.
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}Summary
Rendering diagram...
JS performance improvements
1graph TD
2 Javascript
3 Javascript --> ReduceCPU["Reduce CPU Usage"]
4 Javascript --> ReduceRAM["Reduce RAM usage"]
5 Javascript --> DontBlockUI["Don't block UI thread"]
6 Javascript --> AsyncJobs["Utilize Async jobs"]
7
8 ReduceCPU --> MinimizeSearch["Minimize Search and access cost"]
9 ReduceRAM --> MinimizeRuntime["Minimize runtime state"]
10 MinimizeRuntime --> IndexedDB["Utilize IndexedDB"]
11
12 DontBlockUI --> AvoidRW["Avoid high frequency read-write to sync storages\nlocal storage or \nsession storage"]
13
14 AsyncJobs --> UseServer["Use server"]
15 AsyncJobs --> WebWorkers["Use Web Workers"]Rendering diagram...
1graph TD
2 Javascript
3 Javascript --> ReduceCPU["Reduce CPU Usage"]
4 Javascript --> ReduceRAM["Reduce RAM usage"]
5 Javascript --> DontBlockUI["Don't block UI thread"]
6 Javascript --> AsyncJobs["Utilize Async jobs"]
7
8 ReduceCPU --> MinimizeSearch["Minimize Search and access cost"]
9 ReduceRAM --> MinimizeRuntime["Minimize runtime state"]
10 MinimizeRuntime --> IndexedDB["Utilize IndexedDB"]
11
12 DontBlockUI --> AvoidRW["Avoid high frequency read-write to sync storages\nlocal storage or \nsession storage"]
13
14 AsyncJobs --> UseServer["Use server"]
15 AsyncJobs --> WebWorkers["Use Web Workers"]LCP, (Largest Contentful Paint)
Loading performace of app
Should be < 2.5s
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
Rendering diagram...
Rendering diagram...
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%.
1function helloWold() {
2 const longVariableName = 5 + 5,
3 return longVariableName;
4}
5
6function h(){return 5+5}1function helloWold() {
2 const longVariableName = 5 + 5,
3 return longVariableName;
4}
5
6function h(){return 5+5}Change order of script loading
1<script src="module1.js"></script>
2<script src="anlaytics.js" defer></script>
3<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.
1<script src="module1.js"></script>
2<script src="anlaytics.js" defer></script>
3<script src="module1.js"></script>CSS can be minified and compressed as well.
No title
Rendering diagram...
Inline critical styles
<html>
<head>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>instead do this
<html>
<head>
<style>
/* critical styles */
</style>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>Fetching non-critical styles
<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.
<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
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 --> AVIFCompress SVG's
Use AVIF for better performance
Fonts
@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.
No title
Rendering diagram...
Rendering diagram...
Inline critical styles
1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>instead do this
1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>Fetching non-critical styles
1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</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.
1<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.
1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</html>1<link rel="preload" as="style" href="href://cdn.com/desktop.css" />Rendering diagram...
Fonts
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}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.
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</html>1<link rel="preload" as="style" href="href://cdn.com/desktop.css" />Rendering diagram...
Fonts
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}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.
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}API
Data model
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
Data model
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',
},
},
};1const stories = {
2 story_id: {
3 text: 'Story text',
4 story_id: 'story_id',
5 timestamp: 'timestamp',
6 },
7};
8
9const comments = {
10 story_id: {
11 comment_id: {
12 text: 'Comment text',
13 id: 'id',
14 timestamp: 'timestamp',
15 },
16 },
17};
18
19// 2nf, 3nf looks excessive
20const attachments = {
21 story_id: {
22 attachment_id: {
23 url: 'url',
24 type: 'image',
25 timestamp: 'timestamp',
26 },
27 },
28};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
Short polling / Long polling
Battery drain
Data inefficient
Latency (TCP re-connection)
Simple
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
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
1const stories = {
2 story_id: {
3 text: 'Story text',
4 story_id: 'story_id',
5 timestamp: 'timestamp',
6 },
7};
8
9const comments = {
10 story_id: {
11 comment_id: {
12 text: 'Comment text',
13 id: 'id',
14 timestamp: 'timestamp',
15 },
16 },
17};
18
19// 2nf, 3nf looks excessive
20const attachments = {
21 story_id: {
22 attachment_id: {
23 url: 'url',
24 type: 'image',
25 timestamp: 'timestamp',
26 },
27 },
28};Short polling / Long polling
Battery drain
Data inefficient
Latency (TCP re-connection)
Simple
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
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
1const stories = {
2 story_id: {
3 text: 'Story text',
4 story_id: 'story_id',
5 timestamp: 'timestamp',
6 },
7};
8
9const comments = {
10 story_id: {
11 comment_id: {
12 text: 'Comment text',
13 id: 'id',
14 timestamp: 'timestamp',
15 },
16 },
17};
18
19// 2nf, 3nf looks excessive
20const attachments = {
21 story_id: {
22 attachment_id: {
23 url: 'url',
24 type: 'image',
25 timestamp: 'timestamp',
26 },
27 },
28};1const stories = {
2 story_id: {
3 text: 'Story text',
4 story_id: 'story_id',
5 timestamp: 'timestamp',
6 },
7};
8
9const comments = {
10 story_id: {
11 comment_id: {
12 text: 'Comment text',
13 id: 'id',
14 timestamp: 'timestamp',
15 },
16 },
17};
18
19// 2nf, 3nf looks excessive
20const attachments = {
21 story_id: {
22 attachment_id: {
23 url: 'url',
24 type: 'image',
25 timestamp: 'timestamp',
26 },
27 },
28};1<p>Hi</p>
2Iam a anonymous block elementThe below example is GPU intensive, since the layout does not need to be re-calculated. Only the element needs to be repainted.
1// optimsied reflow
2@keyframes moving-down-fast {
3 from {
4 transform: translateY(0);
5 }
6 to {
7 transform: translateY(100px);
8 }
9}1// optimsied reflow
2@keyframes moving-down-fast {
3 from {
4 transform: translateY(0);
5 }
6 to {
7 transform: translateY(100px);
8 }
9}No title
Rendering diagram...
For some weird reason, both HTMDocument and Element have access to the DOMAPI methods.
1$0.querySelector;
2document.querySelector;Rendering diagram...
1$0.querySelector;
2document.querySelector;Global objects
1console.log(typeof window); // object
2console.log(window.document === document); // true
3console.log(window.document.body); // <body></body>
4console.log(window.document.head); // <head></head>1console.log(typeof window); // object
2console.log(window.document === document); // true
3console.log(window.document.body); // <body></body>
4console.log(window.document.head); // <head></head>DOM Querying
getElementById
1document.getElementById('id'); // O(1)O(1) time complexity
Id namespace is shared, across all components.
Make suer we dont overuse ids.
getElementsByTagName or getElementsByClassName
1document.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
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
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.
getElementById
1document.getElementById('id'); // O(1)O(1) time complexity
Id namespace is shared, across all components.
Make suer we dont overuse ids.
1document.getElementById('id'); // O(1)getElementsByTagName or getElementsByClassName
1document.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.
1document.getElementsByClassName('class'); // O(N)querySelector
1document.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.
1document.querySelector('.class'); // O(N)querySelectorAll
1document.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.
1document.querySelectorAll('.class'); // O(N)Performance optimisations
CSS Selectors
1// this is slow, since we need traverse longer
2div > div > section > .flex > .target
3
4// this is faster (When # is used we can skip the DOM tree traversal, upto the id)
5#container.flex > .targetComplex queries take longer to compute
Use Id's for core containers
CSS selectors are compiled by the browser on run time.
Inserting new elements
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.
<!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>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
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 onceCSS Selectors
1// this is slow, since we need traverse longer
2div > div > section > .flex > .target
3
4// this is faster (When # is used we can skip the DOM tree traversal, upto the id)
5#container.flex > .targetComplex queries take longer to compute
Use Id's for core containers
CSS selectors are compiled by the browser on run time.
1// this is slow, since we need traverse longer
2div > div > section > .flex > .target
3
4// this is faster (When # is used we can skip the DOM tree traversal, upto the id)
5#container.flex > .targetInserting new elements
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.
<!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>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');1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title></title>
7 <link href="./index.css" rel="stylesheet" />
8 </head>
9 <body>
10 <template id="template">
11 <div class="card">
12 <h3 class="card-title"></h3>
13 <div class="card-content">
14 <p class="card-desc"></p>
15 <button>Click me</button>
16 </div>
17 </div>
18 </template>
19 </body>
20 <script src="./index.js"></script>
21</html>1const template = document.getElementById('template');
2
3const createAndAppendCard = (title, desc) => {
4 const card = template.content.cloneNode(true).firstElementChild;
5 // cloneNod(isDeepClone?) deep clone to avoid nested elements from being modified by other clones
6 // template.content.cloneNode(true) returns a DocumentFragment
7 // firstElementChild gets the first child of the template / documentFragment
8 const titleEl = card.querySelector('.card-title');
9 const descEl = card.querySelector('.card-desc');
10
11 titleEl.innerText = title; // will cause reflow avoid
12 descEl.textContent = desc; // will not cause reflow
13
14 document.body.appendChild(card);
15 // O(N), causes re-flow
16};
17
18createAndAppendCard('Title 1', 'Description 1');DocumentFragment for dynamic templates
1const fragment = new DocumentFragment();
2for (let idx = 0; idx < 1000; idx++) {
3 const card = template.content.cloneNode(true).firstElementChild;
4 const titleEl = card.querySelector('.card-title');
5 const descEl = card.querySelector('.card-desc');
6
7 titleEl.innerText = `Title ${idx}`;
8 descEl.textContent = `Description ${idx}`;
9
10 fragment.appendChild(card);
11}
12document.body.appendChild(fragment); // this operation causes re-flow only once1const fragment = new DocumentFragment();
2for (let idx = 0; idx < 1000; idx++) {
3 const card = template.content.cloneNode(true).firstElementChild;
4 const titleEl = card.querySelector('.card-title');
5 const descEl = card.querySelector('.card-desc');
6
7 titleEl.innerText = `Title ${idx}`;
8 descEl.textContent = `Description ${idx}`;
9
10 fragment.appendChild(card);
11}
12document.body.appendChild(fragment); // this operation causes re-flow only once1document.querySelector('.class'); // O(N)querySelectorAll
1document.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.
1document.querySelectorAll('.class'); // O(N)Inserting new elements
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.
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.
<!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>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');1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title></title>
7 <link href="./index.css" rel="stylesheet" />
8 </head>
9 <body>
10 <template id="template">
11 <div class="card">
12 <h3 class="card-title"></h3>
13 <div class="card-content">
14 <p class="card-desc"></p>
15 <button>Click me</button>
16 </div>
17 </div>
18 </template>
19 </body>
20 <script src="./index.js"></script>
21</html>1const template = document.getElementById('template');
2
3const createAndAppendCard = (title, desc) => {
4 const card = template.content.cloneNode(true).firstElementChild;
5 // cloneNod(isDeepClone?) deep clone to avoid nested elements from being modified by other clones
6 // template.content.cloneNode(true) returns a DocumentFragment
7 // firstElementChild gets the first child of the template / documentFragment
8 const titleEl = card.querySelector('.card-title');
9 const descEl = card.querySelector('.card-desc');
10
11 titleEl.innerText = title; // will cause reflow avoid
12 descEl.textContent = desc; // will not cause reflow
13
14 document.body.appendChild(card);
15 // O(N), causes re-flow
16};
17
18createAndAppendCard('Title 1', 'Description 1');DocumentFragment for dynamic templates
1const fragment = new DocumentFragment();
2for (let idx = 0; idx < 1000; idx++) {
3 const card = template.content.cloneNode(true).firstElementChild;
4 const titleEl = card.querySelector('.card-title');
5 const descEl = card.querySelector('.card-desc');
6
7 titleEl.innerText = `Title ${idx}`;
8 descEl.textContent = `Description ${idx}`;
9
10 fragment.appendChild(card);
11}
12document.body.appendChild(fragment); // this operation causes re-flow only once1const fragment = new DocumentFragment();
2for (let idx = 0; idx < 1000; idx++) {
3 const card = template.content.cloneNode(true).firstElementChild;
4 const titleEl = card.querySelector('.card-title');
5 const descEl = card.querySelector('.card-desc');
6
7 titleEl.innerText = `Title ${idx}`;
8 descEl.textContent = `Description ${idx}`;
9
10 fragment.appendChild(card);
11}
12document.body.appendChild(fragment); // this operation causes re-flow only once1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title></title>
7 <link href="./index.css" rel="stylesheet" />
8 </head>
9 <body>
10 <template id="template">
11 <div class="card">
12 <h3 class="card-title"></h3>
13 <div class="card-content">
14 <p class="card-desc"></p>
15 <button>Click me</button>
16 </div>
17 </div>
18 </template>
19 </body>
20 <script src="./index.js"></script>
21</html>1const template = document.getElementById('template');
2
3const createAndAppendCard = (title, desc) => {
4 const card = template.content.cloneNode(true).firstElementChild;
5 // cloneNod(isDeepClone?) deep clone to avoid nested elements from being modified by other clones
6 // template.content.cloneNode(true) returns a DocumentFragment
7 // firstElementChild gets the first child of the template / documentFragment
8 const titleEl = card.querySelector('.card-title');
9 const descEl = card.querySelector('.card-desc');
10
11 titleEl.innerText = title; // will cause reflow avoid
12 descEl.textContent = desc; // will not cause reflow
13
14 document.body.appendChild(card);
15 // O(N), causes re-flow
16};
17
18createAndAppendCard('Title 1', 'Description 1');Observer APIs
Intersection Observers
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
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.
h1 = document.createElement('h1');
h1.textContent = textContent.slice(3);
target.replaceWith(h1);Resize Observers
No title
.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)
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);1const callback = (entries, observer) => {
2 entries.forEach(entry => {
3 if (entry.isIntersecting) {
4 console.log('Element is in view');
5 } else {
6 console.log('Element is out of view');
7 }
8 });
9};
10
11const observer = new IntersectionObserver(callback, {
12 threshold: 0.5,
13 root: document.querySelector('.container'),
14});
15
16observer.observe(document.querySelector('.target'));1type MutationRecord = {
2 type: 'childList' | 'attributes' | 'subtree' | 'characterData';
3 target: Node; // the node that was changed
4 addedNodes: NodeList; // nodes added with notation
5 removedNodes: NodeList; // nodes removed with notation
6 oldValue: string; // prev value
7};
8
9const callback = ((mutationsList) => {
10 mutationsList.forEach(mutation: MutationRecord) => {
11 // do something with the mutation
12 });
13};
14
15let observer = new MutationObserver(callback);
16observer.observe(document.querySelector('.target')!, {
17 childList: true, // tracks only immediate children, has higher priority over subTree
18 attributes: true, // track attribute changes
19 subtree: true, // track changes in descendants
20 characterData: true, // tracks text changes
21 attributesFilter: ['class', 'id'],
22});1h1 = document.createElement('h1');
2h1.textContent = textContent.slice(3);
3target.replaceWith(h1);Resize Observers
No title
1.blog-section {
2 width: 100%; // width of the container should not be auto
3 margin-bottom: $margin-bottom--6;
4 container: blog-section-container;
5 container-type: inline-size;
6
7 &__title {
8 margin-bottom: $margin-bottom--2;
9 }
10}
11
12@container blog-section-container (max-width: 400px) {
13 .blog-section {
14 &__title {
15 color: red;
16 }
17 }
18}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)
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);1.blog-section {
2 width: 100%; // width of the container should not be auto
3 margin-bottom: $margin-bottom--6;
4 container: blog-section-container;
5 container-type: inline-size;
6
7 &__title {
8 margin-bottom: $margin-bottom--2;
9 }
10}
11
12@container blog-section-container (max-width: 400px) {
13 .blog-section {
14 &__title {
15 color: red;
16 }
17 }
18}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)
1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1const callback = (entries, observer) => {
2 entries.forEach(entry => {
3 if (entry.isIntersecting) {
4 console.log('Element is in view');
5 } else {
6 console.log('Element is out of view');
7 }
8 });
9};
10
11const observer = new IntersectionObserver(callback, {
12 threshold: 0.5,
13 root: document.querySelector('.container'),
14});
15
16observer.observe(document.querySelector('.target'));1type MutationRecord = {
2 type: 'childList' | 'attributes' | 'subtree' | 'characterData';
3 target: Node; // the node that was changed
4 addedNodes: NodeList; // nodes added with notation
5 removedNodes: NodeList; // nodes removed with notation
6 oldValue: string; // prev value
7};
8
9const callback = ((mutationsList) => {
10 mutationsList.forEach(mutation: MutationRecord) => {
11 // do something with the mutation
12 });
13};
14
15let observer = new MutationObserver(callback);
16observer.observe(document.querySelector('.target')!, {
17 childList: true, // tracks only immediate children, has higher priority over subTree
18 attributes: true, // track attribute changes
19 subtree: true, // track changes in descendants
20 characterData: true, // tracks text changes
21 attributesFilter: ['class', 'id'],
22});1h1 = document.createElement('h1');
2h1.textContent = textContent.slice(3);
3target.replaceWith(h1);Resize Observers
No title
1.blog-section {
2 width: 100%; // width of the container should not be auto
3 margin-bottom: $margin-bottom--6;
4 container: blog-section-container;
5 container-type: inline-size;
6
7 &__title {
8 margin-bottom: $margin-bottom--2;
9 }
10}
11
12@container blog-section-container (max-width: 400px) {
13 .blog-section {
14 &__title {
15 color: red;
16 }
17 }
18}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)
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);1.blog-section {
2 width: 100%; // width of the container should not be auto
3 margin-bottom: $margin-bottom--6;
4 container: blog-section-container;
5 container-type: inline-size;
6
7 &__title {
8 margin-bottom: $margin-bottom--2;
9 }
10}
11
12@container blog-section-container (max-width: 400px) {
13 .blog-section {
14 &__title {
15 color: red;
16 }
17 }
18}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)
1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1type MutationRecord = {
2 type: 'childList' | 'attributes' | 'subtree' | 'characterData';
3 target: Node; // the node that was changed
4 addedNodes: NodeList; // nodes added with notation
5 removedNodes: NodeList; // nodes removed with notation
6 oldValue: string; // prev value
7};
8
9const callback = ((mutationsList) => {
10 mutationsList.forEach(mutation: MutationRecord) => {
11 // do something with the mutation
12 });
13};
14
15let observer = new MutationObserver(callback);
16observer.observe(document.querySelector('.target')!, {
17 childList: true, // tracks only immediate children, has higher priority over subTree
18 attributes: true, // track attribute changes
19 subtree: true, // track changes in descendants
20 characterData: true, // tracks text changes
21 attributesFilter: ['class', 'id'],
22});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)
1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);Application state design
No title
Rendering diagram...
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)
// 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
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 forHTTP 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?
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.
Rendering diagram...
Data Normalization (1NF, 2NF, 3NF)
// 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
},
]1// 1NF,
2// all keys use a single atomic value
3const user_id_1 = {
4 id: 1, // primary key
5 name: 'John', // atomic
6 age: 30,
7 address_street: '123 Main St',
8 address_city: 'New York',
9 address_state: 'NY',
10 address_zip: '10001'
11};
12
13// 2 NF,
14const users = {
15 1: {
16 id: 1, // primary key
17 name: 'John', // name depends on id, primary key
18 age: 30,
19 address: {
20 street: '123 Main St',
21 // depends on address, address depends on id
22 city: 'New York',
23 state: 'NY',
24 zip: '10001'
25 }
26 },
27]
28
29// 3 NF, (usually excessive on the FE)
30const addresses: {
31 1: {
32 street: '123 Main St',
33 city: 'New York',
34 state: 'NY',
35 zip: '10001'
36 }
37};
38
39const users = {
40 1: {
41 id: 1,
42 name: 'John',
43 age: 30,
44 address: addressee[1] // no transitive dependency
45 },
46]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
No title
Rendering diagram...
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.
No title
Rendering diagram...
HTTP 1.1 doesn't compress headers.
HTTP 2 compress headers,
Rendering diagram...
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.
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?
Rendering diagram...
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?
Rendering diagram...
Rendering diagram...
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.
Data Normalization (1NF, 2NF, 3NF)
// 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
},
]1// 1NF,
2// all keys use a single atomic value
3const user_id_1 = {
4 id: 1, // primary key
5 name: 'John', // atomic
6 age: 30,
7 address_street: '123 Main St',
8 address_city: 'New York',
9 address_state: 'NY',
10 address_zip: '10001'
11};
12
13// 2 NF,
14const users = {
15 1: {
16 id: 1, // primary key
17 name: 'John', // name depends on id, primary key
18 age: 30,
19 address: {
20 street: '123 Main St',
21 // depends on address, address depends on id
22 city: 'New York',
23 state: 'NY',
24 zip: '10001'
25 }
26 },
27]
28
29// 3 NF, (usually excessive on the FE)
30const addresses: {
31 1: {
32 street: '123 Main St',
33 city: 'New York',
34 state: 'NY',
35 zip: '10001'
36 }
37};
38
39const users = {
40 1: {
41 id: 1,
42 name: 'John',
43 age: 30,
44 address: addressee[1] // no transitive dependency
45 },
46]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
No title
Rendering diagram...
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.
No title
Rendering diagram...
HTTP 1.1 doesn't compress headers.
HTTP 2 compress headers,
Rendering diagram...
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.
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?
Rendering diagram...
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?
Rendering diagram...
Rendering diagram...
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.
1// 1NF,
2// all keys use a single atomic value
3const user_id_1 = {
4 id: 1, // primary key
5 name: 'John', // atomic
6 age: 30,
7 address_street: '123 Main St',
8 address_city: 'New York',
9 address_state: 'NY',
10 address_zip: '10001'
11};
12
13// 2 NF,
14const users = {
15 1: {
16 id: 1, // primary key
17 name: 'John', // name depends on id, primary key
18 age: 30,
19 address: {
20 street: '123 Main St',
21 // depends on address, address depends on id
22 city: 'New York',
23 state: 'NY',
24 zip: '10001'
25 }
26 },
27]
28
29// 3 NF, (usually excessive on the FE)
30const addresses: {
31 1: {
32 street: '123 Main St',
33 city: 'New York',
34 state: 'NY',
35 zip: '10001'
36 }
37};
38
39const users = {
40 1: {
41 id: 1,
42 name: 'John',
43 age: 30,
44 address: addressee[1] // no transitive dependency
45 },
46]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
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| ServerSplit 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%.
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
<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.
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| ServerInline critical styles
<html>
<head>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>instead do this
<html>
<head>
<style>
/* critical styles */
</style>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>Fetching non-critical styles
<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.
<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
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 --> AVIFCompress SVG's
Use AVIF for better performance
Fonts
@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.
LCP, (Largest Contentful Paint)
Loading performace of app
Should be < 2.5s
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
Rendering diagram...
Rendering diagram...
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%.
1function helloWold() {
2 const longVariableName = 5 + 5,
3 return longVariableName;
4}
5
6function h(){return 5+5}1function helloWold() {
2 const longVariableName = 5 + 5,
3 return longVariableName;
4}
5
6function h(){return 5+5}Change order of script loading
1<script src="module1.js"></script>
2<script src="anlaytics.js" defer></script>
3<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.
1<script src="module1.js"></script>
2<script src="anlaytics.js" defer></script>
3<script src="module1.js"></script>CSS can be minified and compressed as well.
No title
Rendering diagram...
Inline critical styles
<html>
<head>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>instead do this
<html>
<head>
<style>
/* critical styles */
</style>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>Fetching non-critical styles
<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.
<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
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 --> AVIFCompress SVG's
Use AVIF for better performance
Fonts
@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.
No title
Rendering diagram...
Rendering diagram...
Inline critical styles
1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>instead do this
1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>Fetching non-critical styles
1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</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.
1<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.
1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</html>1<link rel="preload" as="style" href="href://cdn.com/desktop.css" />Rendering diagram...
Fonts
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}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.
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}Summary
Rendering diagram...
JS performance improvements
1graph TD
2 Javascript
3 Javascript --> ReduceCPU["Reduce CPU Usage"]
4 Javascript --> ReduceRAM["Reduce RAM usage"]
5 Javascript --> DontBlockUI["Don't block UI thread"]
6 Javascript --> AsyncJobs["Utilize Async jobs"]
7
8 ReduceCPU --> MinimizeSearch["Minimize Search and access cost"]
9 ReduceRAM --> MinimizeRuntime["Minimize runtime state"]
10 MinimizeRuntime --> IndexedDB["Utilize IndexedDB"]
11
12 DontBlockUI --> AvoidRW["Avoid high frequency read-write to sync storages\nlocal storage or \nsession storage"]
13
14 AsyncJobs --> UseServer["Use server"]
15 AsyncJobs --> WebWorkers["Use Web Workers"]Rendering diagram...
1graph TD
2 Javascript
3 Javascript --> ReduceCPU["Reduce CPU Usage"]
4 Javascript --> ReduceRAM["Reduce RAM usage"]
5 Javascript --> DontBlockUI["Don't block UI thread"]
6 Javascript --> AsyncJobs["Utilize Async jobs"]
7
8 ReduceCPU --> MinimizeSearch["Minimize Search and access cost"]
9 ReduceRAM --> MinimizeRuntime["Minimize runtime state"]
10 MinimizeRuntime --> IndexedDB["Utilize IndexedDB"]
11
12 DontBlockUI --> AvoidRW["Avoid high frequency read-write to sync storages\nlocal storage or \nsession storage"]
13
14 AsyncJobs --> UseServer["Use server"]
15 AsyncJobs --> WebWorkers["Use Web Workers"]LCP, (Largest Contentful Paint)
Loading performace of app
Should be < 2.5s
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
Rendering diagram...
Rendering diagram...
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%.
1function helloWold() {
2 const longVariableName = 5 + 5,
3 return longVariableName;
4}
5
6function h(){return 5+5}1function helloWold() {
2 const longVariableName = 5 + 5,
3 return longVariableName;
4}
5
6function h(){return 5+5}Change order of script loading
1<script src="module1.js"></script>
2<script src="anlaytics.js" defer></script>
3<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.
1<script src="module1.js"></script>
2<script src="anlaytics.js" defer></script>
3<script src="module1.js"></script>CSS can be minified and compressed as well.
No title
Rendering diagram...
Inline critical styles
<html>
<head>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>instead do this
<html>
<head>
<style>
/* critical styles */
</style>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>Fetching non-critical styles
<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.
<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
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 --> AVIFCompress SVG's
Use AVIF for better performance
Fonts
@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.
No title
Rendering diagram...
Rendering diagram...
Inline critical styles
1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>instead do this
1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>Fetching non-critical styles
1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</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.
1<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.
1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</html>1<link rel="preload" as="style" href="href://cdn.com/desktop.css" />Rendering diagram...
Fonts
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}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.
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</html>1<link rel="preload" as="style" href="href://cdn.com/desktop.css" />Rendering diagram...
Fonts
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}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.
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}API
Data model
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
Data model
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',
},
},
};1const stories = {
2 story_id: {
3 text: 'Story text',
4 story_id: 'story_id',
5 timestamp: 'timestamp',
6 },
7};
8
9const comments = {
10 story_id: {
11 comment_id: {
12 text: 'Comment text',
13 id: 'id',
14 timestamp: 'timestamp',
15 },
16 },
17};
18
19// 2nf, 3nf looks excessive
20const attachments = {
21 story_id: {
22 attachment_id: {
23 url: 'url',
24 type: 'image',
25 timestamp: 'timestamp',
26 },
27 },
28};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
Short polling / Long polling
Battery drain
Data inefficient
Latency (TCP re-connection)
Simple
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
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
1const stories = {
2 story_id: {
3 text: 'Story text',
4 story_id: 'story_id',
5 timestamp: 'timestamp',
6 },
7};
8
9const comments = {
10 story_id: {
11 comment_id: {
12 text: 'Comment text',
13 id: 'id',
14 timestamp: 'timestamp',
15 },
16 },
17};
18
19// 2nf, 3nf looks excessive
20const attachments = {
21 story_id: {
22 attachment_id: {
23 url: 'url',
24 type: 'image',
25 timestamp: 'timestamp',
26 },
27 },
28};Short polling / Long polling
Battery drain
Data inefficient
Latency (TCP re-connection)
Simple
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
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
1const stories = {
2 story_id: {
3 text: 'Story text',
4 story_id: 'story_id',
5 timestamp: 'timestamp',
6 },
7};
8
9const comments = {
10 story_id: {
11 comment_id: {
12 text: 'Comment text',
13 id: 'id',
14 timestamp: 'timestamp',
15 },
16 },
17};
18
19// 2nf, 3nf looks excessive
20const attachments = {
21 story_id: {
22 attachment_id: {
23 url: 'url',
24 type: 'image',
25 timestamp: 'timestamp',
26 },
27 },
28};1const stories = {
2 story_id: {
3 text: 'Story text',
4 story_id: 'story_id',
5 timestamp: 'timestamp',
6 },
7};
8
9const comments = {
10 story_id: {
11 comment_id: {
12 text: 'Comment text',
13 id: 'id',
14 timestamp: 'timestamp',
15 },
16 },
17};
18
19// 2nf, 3nf looks excessive
20const attachments = {
21 story_id: {
22 attachment_id: {
23 url: 'url',
24 type: 'image',
25 timestamp: 'timestamp',
26 },
27 },
28};1document.querySelector('.class'); // O(N)querySelectorAll
1document.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.
1document.querySelectorAll('.class'); // O(N)Inserting new elements
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.
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.
<!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>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');1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title></title>
7 <link href="./index.css" rel="stylesheet" />
8 </head>
9 <body>
10 <template id="template">
11 <div class="card">
12 <h3 class="card-title"></h3>
13 <div class="card-content">
14 <p class="card-desc"></p>
15 <button>Click me</button>
16 </div>
17 </div>
18 </template>
19 </body>
20 <script src="./index.js"></script>
21</html>1const template = document.getElementById('template');
2
3const createAndAppendCard = (title, desc) => {
4 const card = template.content.cloneNode(true).firstElementChild;
5 // cloneNod(isDeepClone?) deep clone to avoid nested elements from being modified by other clones
6 // template.content.cloneNode(true) returns a DocumentFragment
7 // firstElementChild gets the first child of the template / documentFragment
8 const titleEl = card.querySelector('.card-title');
9 const descEl = card.querySelector('.card-desc');
10
11 titleEl.innerText = title; // will cause reflow avoid
12 descEl.textContent = desc; // will not cause reflow
13
14 document.body.appendChild(card);
15 // O(N), causes re-flow
16};
17
18createAndAppendCard('Title 1', 'Description 1');DocumentFragment for dynamic templates
1const fragment = new DocumentFragment();
2for (let idx = 0; idx < 1000; idx++) {
3 const card = template.content.cloneNode(true).firstElementChild;
4 const titleEl = card.querySelector('.card-title');
5 const descEl = card.querySelector('.card-desc');
6
7 titleEl.innerText = `Title ${idx}`;
8 descEl.textContent = `Description ${idx}`;
9
10 fragment.appendChild(card);
11}
12document.body.appendChild(fragment); // this operation causes re-flow only once1const fragment = new DocumentFragment();
2for (let idx = 0; idx < 1000; idx++) {
3 const card = template.content.cloneNode(true).firstElementChild;
4 const titleEl = card.querySelector('.card-title');
5 const descEl = card.querySelector('.card-desc');
6
7 titleEl.innerText = `Title ${idx}`;
8 descEl.textContent = `Description ${idx}`;
9
10 fragment.appendChild(card);
11}
12document.body.appendChild(fragment); // this operation causes re-flow only once1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title></title>
7 <link href="./index.css" rel="stylesheet" />
8 </head>
9 <body>
10 <template id="template">
11 <div class="card">
12 <h3 class="card-title"></h3>
13 <div class="card-content">
14 <p class="card-desc"></p>
15 <button>Click me</button>
16 </div>
17 </div>
18 </template>
19 </body>
20 <script src="./index.js"></script>
21</html>1const template = document.getElementById('template');
2
3const createAndAppendCard = (title, desc) => {
4 const card = template.content.cloneNode(true).firstElementChild;
5 // cloneNod(isDeepClone?) deep clone to avoid nested elements from being modified by other clones
6 // template.content.cloneNode(true) returns a DocumentFragment
7 // firstElementChild gets the first child of the template / documentFragment
8 const titleEl = card.querySelector('.card-title');
9 const descEl = card.querySelector('.card-desc');
10
11 titleEl.innerText = title; // will cause reflow avoid
12 descEl.textContent = desc; // will not cause reflow
13
14 document.body.appendChild(card);
15 // O(N), causes re-flow
16};
17
18createAndAppendCard('Title 1', 'Description 1');1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title></title>
7 <link href="./index.css" rel="stylesheet" />
8 </head>
9 <body>
10 <template id="template">
11 <div class="card">
12 <h3 class="card-title"></h3>
13 <div class="card-content">
14 <p class="card-desc"></p>
15 <button>Click me</button>
16 </div>
17 </div>
18 </template>
19 </body>
20 <script src="./index.js"></script>
21</html>1const template = document.getElementById('template');
2
3const createAndAppendCard = (title, desc) => {
4 const card = template.content.cloneNode(true).firstElementChild;
5 // cloneNod(isDeepClone?) deep clone to avoid nested elements from being modified by other clones
6 // template.content.cloneNode(true) returns a DocumentFragment
7 // firstElementChild gets the first child of the template / documentFragment
8 const titleEl = card.querySelector('.card-title');
9 const descEl = card.querySelector('.card-desc');
10
11 titleEl.innerText = title; // will cause reflow avoid
12 descEl.textContent = desc; // will not cause reflow
13
14 document.body.appendChild(card);
15 // O(N), causes re-flow
16};
17
18createAndAppendCard('Title 1', 'Description 1');1const callback = (entries, observer) => {
2 entries.forEach(entry => {
3 if (entry.isIntersecting) {
4 console.log('Element is in view');
5 } else {
6 console.log('Element is out of view');
7 }
8 });
9};
10
11const observer = new IntersectionObserver(callback, {
12 threshold: 0.5,
13 root: document.querySelector('.container'),
14});
15
16observer.observe(document.querySelector('.target'));1type MutationRecord = {
2 type: 'childList' | 'attributes' | 'subtree' | 'characterData';
3 target: Node; // the node that was changed
4 addedNodes: NodeList; // nodes added with notation
5 removedNodes: NodeList; // nodes removed with notation
6 oldValue: string; // prev value
7};
8
9const callback = ((mutationsList) => {
10 mutationsList.forEach(mutation: MutationRecord) => {
11 // do something with the mutation
12 });
13};
14
15let observer = new MutationObserver(callback);
16observer.observe(document.querySelector('.target')!, {
17 childList: true, // tracks only immediate children, has higher priority over subTree
18 attributes: true, // track attribute changes
19 subtree: true, // track changes in descendants
20 characterData: true, // tracks text changes
21 attributesFilter: ['class', 'id'],
22});1h1 = document.createElement('h1');
2h1.textContent = textContent.slice(3);
3target.replaceWith(h1);Resize Observers
No title
1.blog-section {
2 width: 100%; // width of the container should not be auto
3 margin-bottom: $margin-bottom--6;
4 container: blog-section-container;
5 container-type: inline-size;
6
7 &__title {
8 margin-bottom: $margin-bottom--2;
9 }
10}
11
12@container blog-section-container (max-width: 400px) {
13 .blog-section {
14 &__title {
15 color: red;
16 }
17 }
18}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)
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);1.blog-section {
2 width: 100%; // width of the container should not be auto
3 margin-bottom: $margin-bottom--6;
4 container: blog-section-container;
5 container-type: inline-size;
6
7 &__title {
8 margin-bottom: $margin-bottom--2;
9 }
10}
11
12@container blog-section-container (max-width: 400px) {
13 .blog-section {
14 &__title {
15 color: red;
16 }
17 }
18}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)
1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);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)
1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);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)
1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);Data Normalization (1NF, 2NF, 3NF)
// 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
},
]1// 1NF,
2// all keys use a single atomic value
3const user_id_1 = {
4 id: 1, // primary key
5 name: 'John', // atomic
6 age: 30,
7 address_street: '123 Main St',
8 address_city: 'New York',
9 address_state: 'NY',
10 address_zip: '10001'
11};
12
13// 2 NF,
14const users = {
15 1: {
16 id: 1, // primary key
17 name: 'John', // name depends on id, primary key
18 age: 30,
19 address: {
20 street: '123 Main St',
21 // depends on address, address depends on id
22 city: 'New York',
23 state: 'NY',
24 zip: '10001'
25 }
26 },
27]
28
29// 3 NF, (usually excessive on the FE)
30const addresses: {
31 1: {
32 street: '123 Main St',
33 city: 'New York',
34 state: 'NY',
35 zip: '10001'
36 }
37};
38
39const users = {
40 1: {
41 id: 1,
42 name: 'John',
43 age: 30,
44 address: addressee[1] // no transitive dependency
45 },
46]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
No title
Rendering diagram...
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.
No title
Rendering diagram...
HTTP 1.1 doesn't compress headers.
HTTP 2 compress headers,
Rendering diagram...
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.
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?
Rendering diagram...
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?
Rendering diagram...
Rendering diagram...
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.
1// 1NF,
2// all keys use a single atomic value
3const user_id_1 = {
4 id: 1, // primary key
5 name: 'John', // atomic
6 age: 30,
7 address_street: '123 Main St',
8 address_city: 'New York',
9 address_state: 'NY',
10 address_zip: '10001'
11};
12
13// 2 NF,
14const users = {
15 1: {
16 id: 1, // primary key
17 name: 'John', // name depends on id, primary key
18 age: 30,
19 address: {
20 street: '123 Main St',
21 // depends on address, address depends on id
22 city: 'New York',
23 state: 'NY',
24 zip: '10001'
25 }
26 },
27]
28
29// 3 NF, (usually excessive on the FE)
30const addresses: {
31 1: {
32 street: '123 Main St',
33 city: 'New York',
34 state: 'NY',
35 zip: '10001'
36 }
37};
38
39const users = {
40 1: {
41 id: 1,
42 name: 'John',
43 age: 30,
44 address: addressee[1] // no transitive dependency
45 },
46]1// 1NF,
2// all keys use a single atomic value
3const user_id_1 = {
4 id: 1, // primary key
5 name: 'John', // atomic
6 age: 30,
7 address_street: '123 Main St',
8 address_city: 'New York',
9 address_state: 'NY',
10 address_zip: '10001'
11};
12
13// 2 NF,
14const users = {
15 1: {
16 id: 1, // primary key
17 name: 'John', // name depends on id, primary key
18 age: 30,
19 address: {
20 street: '123 Main St',
21 // depends on address, address depends on id
22 city: 'New York',
23 state: 'NY',
24 zip: '10001'
25 }
26 },
27]
28
29// 3 NF, (usually excessive on the FE)
30const addresses: {
31 1: {
32 street: '123 Main St',
33 city: 'New York',
34 state: 'NY',
35 zip: '10001'
36 }
37};
38
39const users = {
40 1: {
41 id: 1,
42 name: 'John',
43 age: 30,
44 address: addressee[1] // no transitive dependency
45 },
46]LCP, (Largest Contentful Paint)
Loading performace of app
Should be < 2.5s
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
Rendering diagram...
Rendering diagram...
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%.
1function helloWold() {
2 const longVariableName = 5 + 5,
3 return longVariableName;
4}
5
6function h(){return 5+5}1function helloWold() {
2 const longVariableName = 5 + 5,
3 return longVariableName;
4}
5
6function h(){return 5+5}Change order of script loading
1<script src="module1.js"></script>
2<script src="anlaytics.js" defer></script>
3<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.
1<script src="module1.js"></script>
2<script src="anlaytics.js" defer></script>
3<script src="module1.js"></script>CSS can be minified and compressed as well.
No title
Rendering diagram...
Inline critical styles
<html>
<head>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>instead do this
<html>
<head>
<style>
/* critical styles */
</style>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>Fetching non-critical styles
<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.
<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
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 --> AVIFCompress SVG's
Use AVIF for better performance
Fonts
@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.
No title
Rendering diagram...
Rendering diagram...
Inline critical styles
1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>instead do this
1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>Fetching non-critical styles
1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</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.
1<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.
1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</html>1<link rel="preload" as="style" href="href://cdn.com/desktop.css" />Rendering diagram...
Fonts
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}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.
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</html>1<link rel="preload" as="style" href="href://cdn.com/desktop.css" />Rendering diagram...
Fonts
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}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.
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</html>1<link rel="preload" as="style" href="href://cdn.com/desktop.css" />Rendering diagram...
Fonts
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}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.
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}1const stories = {
2 story_id: {
3 text: 'Story text',
4 story_id: 'story_id',
5 timestamp: 'timestamp',
6 },
7};
8
9const comments = {
10 story_id: {
11 comment_id: {
12 text: 'Comment text',
13 id: 'id',
14 timestamp: 'timestamp',
15 },
16 },
17};
18
19// 2nf, 3nf looks excessive
20const attachments = {
21 story_id: {
22 attachment_id: {
23 url: 'url',
24 type: 'image',
25 timestamp: 'timestamp',
26 },
27 },
28};1const stories = {
2 story_id: {
3 text: 'Story text',
4 story_id: 'story_id',
5 timestamp: 'timestamp',
6 },
7};
8
9const comments = {
10 story_id: {
11 comment_id: {
12 text: 'Comment text',
13 id: 'id',
14 timestamp: 'timestamp',
15 },
16 },
17};
18
19// 2nf, 3nf looks excessive
20const attachments = {
21 story_id: {
22 attachment_id: {
23 url: 'url',
24 type: 'image',
25 timestamp: 'timestamp',
26 },
27 },
28};1document.querySelector('.class'); // O(N)querySelectorAll
1document.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.
1document.querySelectorAll('.class'); // O(N)Inserting new elements
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.
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.
<!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>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');1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title></title>
7 <link href="./index.css" rel="stylesheet" />
8 </head>
9 <body>
10 <template id="template">
11 <div class="card">
12 <h3 class="card-title"></h3>
13 <div class="card-content">
14 <p class="card-desc"></p>
15 <button>Click me</button>
16 </div>
17 </div>
18 </template>
19 </body>
20 <script src="./index.js"></script>
21</html>1const template = document.getElementById('template');
2
3const createAndAppendCard = (title, desc) => {
4 const card = template.content.cloneNode(true).firstElementChild;
5 // cloneNod(isDeepClone?) deep clone to avoid nested elements from being modified by other clones
6 // template.content.cloneNode(true) returns a DocumentFragment
7 // firstElementChild gets the first child of the template / documentFragment
8 const titleEl = card.querySelector('.card-title');
9 const descEl = card.querySelector('.card-desc');
10
11 titleEl.innerText = title; // will cause reflow avoid
12 descEl.textContent = desc; // will not cause reflow
13
14 document.body.appendChild(card);
15 // O(N), causes re-flow
16};
17
18createAndAppendCard('Title 1', 'Description 1');DocumentFragment for dynamic templates
1const fragment = new DocumentFragment();
2for (let idx = 0; idx < 1000; idx++) {
3 const card = template.content.cloneNode(true).firstElementChild;
4 const titleEl = card.querySelector('.card-title');
5 const descEl = card.querySelector('.card-desc');
6
7 titleEl.innerText = `Title ${idx}`;
8 descEl.textContent = `Description ${idx}`;
9
10 fragment.appendChild(card);
11}
12document.body.appendChild(fragment); // this operation causes re-flow only once1const fragment = new DocumentFragment();
2for (let idx = 0; idx < 1000; idx++) {
3 const card = template.content.cloneNode(true).firstElementChild;
4 const titleEl = card.querySelector('.card-title');
5 const descEl = card.querySelector('.card-desc');
6
7 titleEl.innerText = `Title ${idx}`;
8 descEl.textContent = `Description ${idx}`;
9
10 fragment.appendChild(card);
11}
12document.body.appendChild(fragment); // this operation causes re-flow only once1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title></title>
7 <link href="./index.css" rel="stylesheet" />
8 </head>
9 <body>
10 <template id="template">
11 <div class="card">
12 <h3 class="card-title"></h3>
13 <div class="card-content">
14 <p class="card-desc"></p>
15 <button>Click me</button>
16 </div>
17 </div>
18 </template>
19 </body>
20 <script src="./index.js"></script>
21</html>1const template = document.getElementById('template');
2
3const createAndAppendCard = (title, desc) => {
4 const card = template.content.cloneNode(true).firstElementChild;
5 // cloneNod(isDeepClone?) deep clone to avoid nested elements from being modified by other clones
6 // template.content.cloneNode(true) returns a DocumentFragment
7 // firstElementChild gets the first child of the template / documentFragment
8 const titleEl = card.querySelector('.card-title');
9 const descEl = card.querySelector('.card-desc');
10
11 titleEl.innerText = title; // will cause reflow avoid
12 descEl.textContent = desc; // will not cause reflow
13
14 document.body.appendChild(card);
15 // O(N), causes re-flow
16};
17
18createAndAppendCard('Title 1', 'Description 1');1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title></title>
7 <link href="./index.css" rel="stylesheet" />
8 </head>
9 <body>
10 <template id="template">
11 <div class="card">
12 <h3 class="card-title"></h3>
13 <div class="card-content">
14 <p class="card-desc"></p>
15 <button>Click me</button>
16 </div>
17 </div>
18 </template>
19 </body>
20 <script src="./index.js"></script>
21</html>1const template = document.getElementById('template');
2
3const createAndAppendCard = (title, desc) => {
4 const card = template.content.cloneNode(true).firstElementChild;
5 // cloneNod(isDeepClone?) deep clone to avoid nested elements from being modified by other clones
6 // template.content.cloneNode(true) returns a DocumentFragment
7 // firstElementChild gets the first child of the template / documentFragment
8 const titleEl = card.querySelector('.card-title');
9 const descEl = card.querySelector('.card-desc');
10
11 titleEl.innerText = title; // will cause reflow avoid
12 descEl.textContent = desc; // will not cause reflow
13
14 document.body.appendChild(card);
15 // O(N), causes re-flow
16};
17
18createAndAppendCard('Title 1', 'Description 1');1const callback = (entries, observer) => {
2 entries.forEach(entry => {
3 if (entry.isIntersecting) {
4 console.log('Element is in view');
5 } else {
6 console.log('Element is out of view');
7 }
8 });
9};
10
11const observer = new IntersectionObserver(callback, {
12 threshold: 0.5,
13 root: document.querySelector('.container'),
14});
15
16observer.observe(document.querySelector('.target'));1type MutationRecord = {
2 type: 'childList' | 'attributes' | 'subtree' | 'characterData';
3 target: Node; // the node that was changed
4 addedNodes: NodeList; // nodes added with notation
5 removedNodes: NodeList; // nodes removed with notation
6 oldValue: string; // prev value
7};
8
9const callback = ((mutationsList) => {
10 mutationsList.forEach(mutation: MutationRecord) => {
11 // do something with the mutation
12 });
13};
14
15let observer = new MutationObserver(callback);
16observer.observe(document.querySelector('.target')!, {
17 childList: true, // tracks only immediate children, has higher priority over subTree
18 attributes: true, // track attribute changes
19 subtree: true, // track changes in descendants
20 characterData: true, // tracks text changes
21 attributesFilter: ['class', 'id'],
22});1h1 = document.createElement('h1');
2h1.textContent = textContent.slice(3);
3target.replaceWith(h1);Resize Observers
No title
1.blog-section {
2 width: 100%; // width of the container should not be auto
3 margin-bottom: $margin-bottom--6;
4 container: blog-section-container;
5 container-type: inline-size;
6
7 &__title {
8 margin-bottom: $margin-bottom--2;
9 }
10}
11
12@container blog-section-container (max-width: 400px) {
13 .blog-section {
14 &__title {
15 color: red;
16 }
17 }
18}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)
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);1.blog-section {
2 width: 100%; // width of the container should not be auto
3 margin-bottom: $margin-bottom--6;
4 container: blog-section-container;
5 container-type: inline-size;
6
7 &__title {
8 margin-bottom: $margin-bottom--2;
9 }
10}
11
12@container blog-section-container (max-width: 400px) {
13 .blog-section {
14 &__title {
15 color: red;
16 }
17 }
18}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)
1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);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)
1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);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)
1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);Data Normalization (1NF, 2NF, 3NF)
// 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
},
]1// 1NF,
2// all keys use a single atomic value
3const user_id_1 = {
4 id: 1, // primary key
5 name: 'John', // atomic
6 age: 30,
7 address_street: '123 Main St',
8 address_city: 'New York',
9 address_state: 'NY',
10 address_zip: '10001'
11};
12
13// 2 NF,
14const users = {
15 1: {
16 id: 1, // primary key
17 name: 'John', // name depends on id, primary key
18 age: 30,
19 address: {
20 street: '123 Main St',
21 // depends on address, address depends on id
22 city: 'New York',
23 state: 'NY',
24 zip: '10001'
25 }
26 },
27]
28
29// 3 NF, (usually excessive on the FE)
30const addresses: {
31 1: {
32 street: '123 Main St',
33 city: 'New York',
34 state: 'NY',
35 zip: '10001'
36 }
37};
38
39const users = {
40 1: {
41 id: 1,
42 name: 'John',
43 age: 30,
44 address: addressee[1] // no transitive dependency
45 },
46]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
No title
Rendering diagram...
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.
No title
Rendering diagram...
HTTP 1.1 doesn't compress headers.
HTTP 2 compress headers,
Rendering diagram...
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.
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?
Rendering diagram...
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?
Rendering diagram...
Rendering diagram...
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.
1// 1NF,
2// all keys use a single atomic value
3const user_id_1 = {
4 id: 1, // primary key
5 name: 'John', // atomic
6 age: 30,
7 address_street: '123 Main St',
8 address_city: 'New York',
9 address_state: 'NY',
10 address_zip: '10001'
11};
12
13// 2 NF,
14const users = {
15 1: {
16 id: 1, // primary key
17 name: 'John', // name depends on id, primary key
18 age: 30,
19 address: {
20 street: '123 Main St',
21 // depends on address, address depends on id
22 city: 'New York',
23 state: 'NY',
24 zip: '10001'
25 }
26 },
27]
28
29// 3 NF, (usually excessive on the FE)
30const addresses: {
31 1: {
32 street: '123 Main St',
33 city: 'New York',
34 state: 'NY',
35 zip: '10001'
36 }
37};
38
39const users = {
40 1: {
41 id: 1,
42 name: 'John',
43 age: 30,
44 address: addressee[1] // no transitive dependency
45 },
46]1// 1NF,
2// all keys use a single atomic value
3const user_id_1 = {
4 id: 1, // primary key
5 name: 'John', // atomic
6 age: 30,
7 address_street: '123 Main St',
8 address_city: 'New York',
9 address_state: 'NY',
10 address_zip: '10001'
11};
12
13// 2 NF,
14const users = {
15 1: {
16 id: 1, // primary key
17 name: 'John', // name depends on id, primary key
18 age: 30,
19 address: {
20 street: '123 Main St',
21 // depends on address, address depends on id
22 city: 'New York',
23 state: 'NY',
24 zip: '10001'
25 }
26 },
27]
28
29// 3 NF, (usually excessive on the FE)
30const addresses: {
31 1: {
32 street: '123 Main St',
33 city: 'New York',
34 state: 'NY',
35 zip: '10001'
36 }
37};
38
39const users = {
40 1: {
41 id: 1,
42 name: 'John',
43 age: 30,
44 address: addressee[1] // no transitive dependency
45 },
46]LCP, (Largest Contentful Paint)
Loading performace of app
Should be < 2.5s
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
Rendering diagram...
Rendering diagram...
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%.
1function helloWold() {
2 const longVariableName = 5 + 5,
3 return longVariableName;
4}
5
6function h(){return 5+5}1function helloWold() {
2 const longVariableName = 5 + 5,
3 return longVariableName;
4}
5
6function h(){return 5+5}Change order of script loading
1<script src="module1.js"></script>
2<script src="anlaytics.js" defer></script>
3<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.
1<script src="module1.js"></script>
2<script src="anlaytics.js" defer></script>
3<script src="module1.js"></script>CSS can be minified and compressed as well.
No title
Rendering diagram...
Inline critical styles
<html>
<head>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>instead do this
<html>
<head>
<style>
/* critical styles */
</style>
<link rel="stylesheet" href="href://cdn.com/desktop.css" />
</head>
</html>Fetching non-critical styles
<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.
<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
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 --> AVIFCompress SVG's
Use AVIF for better performance
Fonts
@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.
No title
Rendering diagram...
Rendering diagram...
Inline critical styles
1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>instead do this
1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>Fetching non-critical styles
1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</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.
1<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.
1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</html>1<link rel="preload" as="style" href="href://cdn.com/desktop.css" />Rendering diagram...
Fonts
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}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.
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</html>1<link rel="preload" as="style" href="href://cdn.com/desktop.css" />Rendering diagram...
Fonts
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}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.
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</html>1<link rel="preload" as="style" href="href://cdn.com/desktop.css" />Rendering diagram...
Fonts
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}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.
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}1const stories = {
2 story_id: {
3 text: 'Story text',
4 story_id: 'story_id',
5 timestamp: 'timestamp',
6 },
7};
8
9const comments = {
10 story_id: {
11 comment_id: {
12 text: 'Comment text',
13 id: 'id',
14 timestamp: 'timestamp',
15 },
16 },
17};
18
19// 2nf, 3nf looks excessive
20const attachments = {
21 story_id: {
22 attachment_id: {
23 url: 'url',
24 type: 'image',
25 timestamp: 'timestamp',
26 },
27 },
28};1const stories = {
2 story_id: {
3 text: 'Story text',
4 story_id: 'story_id',
5 timestamp: 'timestamp',
6 },
7};
8
9const comments = {
10 story_id: {
11 comment_id: {
12 text: 'Comment text',
13 id: 'id',
14 timestamp: 'timestamp',
15 },
16 },
17};
18
19// 2nf, 3nf looks excessive
20const attachments = {
21 story_id: {
22 attachment_id: {
23 url: 'url',
24 type: 'image',
25 timestamp: 'timestamp',
26 },
27 },
28};1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title></title>
7 <link href="./index.css" rel="stylesheet" />
8 </head>
9 <body>
10 <template id="template">
11 <div class="card">
12 <h3 class="card-title"></h3>
13 <div class="card-content">
14 <p class="card-desc"></p>
15 <button>Click me</button>
16 </div>
17 </div>
18 </template>
19 </body>
20 <script src="./index.js"></script>
21</html>1const template = document.getElementById('template');
2
3const createAndAppendCard = (title, desc) => {
4 const card = template.content.cloneNode(true).firstElementChild;
5 // cloneNod(isDeepClone?) deep clone to avoid nested elements from being modified by other clones
6 // template.content.cloneNode(true) returns a DocumentFragment
7 // firstElementChild gets the first child of the template / documentFragment
8 const titleEl = card.querySelector('.card-title');
9 const descEl = card.querySelector('.card-desc');
10
11 titleEl.innerText = title; // will cause reflow avoid
12 descEl.textContent = desc; // will not cause reflow
13
14 document.body.appendChild(card);
15 // O(N), causes re-flow
16};
17
18createAndAppendCard('Title 1', 'Description 1');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)
1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1// 1NF,
2// all keys use a single atomic value
3const user_id_1 = {
4 id: 1, // primary key
5 name: 'John', // atomic
6 age: 30,
7 address_street: '123 Main St',
8 address_city: 'New York',
9 address_state: 'NY',
10 address_zip: '10001'
11};
12
13// 2 NF,
14const users = {
15 1: {
16 id: 1, // primary key
17 name: 'John', // name depends on id, primary key
18 age: 30,
19 address: {
20 street: '123 Main St',
21 // depends on address, address depends on id
22 city: 'New York',
23 state: 'NY',
24 zip: '10001'
25 }
26 },
27]
28
29// 3 NF, (usually excessive on the FE)
30const addresses: {
31 1: {
32 street: '123 Main St',
33 city: 'New York',
34 state: 'NY',
35 zip: '10001'
36 }
37};
38
39const users = {
40 1: {
41 id: 1,
42 name: 'John',
43 age: 30,
44 address: addressee[1] // no transitive dependency
45 },
46]1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</html>1<link rel="preload" as="style" href="href://cdn.com/desktop.css" />Rendering diagram...
Fonts
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}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.
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title></title>
7 <link href="./index.css" rel="stylesheet" />
8 </head>
9 <body>
10 <template id="template">
11 <div class="card">
12 <h3 class="card-title"></h3>
13 <div class="card-content">
14 <p class="card-desc"></p>
15 <button>Click me</button>
16 </div>
17 </div>
18 </template>
19 </body>
20 <script src="./index.js"></script>
21</html>1const template = document.getElementById('template');
2
3const createAndAppendCard = (title, desc) => {
4 const card = template.content.cloneNode(true).firstElementChild;
5 // cloneNod(isDeepClone?) deep clone to avoid nested elements from being modified by other clones
6 // template.content.cloneNode(true) returns a DocumentFragment
7 // firstElementChild gets the first child of the template / documentFragment
8 const titleEl = card.querySelector('.card-title');
9 const descEl = card.querySelector('.card-desc');
10
11 titleEl.innerText = title; // will cause reflow avoid
12 descEl.textContent = desc; // will not cause reflow
13
14 document.body.appendChild(card);
15 // O(N), causes re-flow
16};
17
18createAndAppendCard('Title 1', 'Description 1');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)
1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1const container = document.querySelector('.container');
2// we may need to de-bounce the resize event,
3// since it can fire multiple times in a short period of time, on resize
4const resizeObserver = new ResizeObserver(entries => {
5 for (let entry of entries) {
6 const { width, height } = entry.contentRect;
7 console.log(`Container resized to ${width}px x ${height}px`);
8 }
9});
10
11resizeObserver.observe(container);1// 1NF,
2// all keys use a single atomic value
3const user_id_1 = {
4 id: 1, // primary key
5 name: 'John', // atomic
6 age: 30,
7 address_street: '123 Main St',
8 address_city: 'New York',
9 address_state: 'NY',
10 address_zip: '10001'
11};
12
13// 2 NF,
14const users = {
15 1: {
16 id: 1, // primary key
17 name: 'John', // name depends on id, primary key
18 age: 30,
19 address: {
20 street: '123 Main St',
21 // depends on address, address depends on id
22 city: 'New York',
23 state: 'NY',
24 zip: '10001'
25 }
26 },
27]
28
29// 3 NF, (usually excessive on the FE)
30const addresses: {
31 1: {
32 street: '123 Main St',
33 city: 'New York',
34 state: 'NY',
35 zip: '10001'
36 }
37};
38
39const users = {
40 1: {
41 id: 1,
42 name: 'John',
43 age: 30,
44 address: addressee[1] // no transitive dependency
45 },
46]1<html>
2 <head>
3 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
4 </head>
5</html>1<html>
2 <head>
3 <style>
4 /* critical styles */
5 </style>
6 <link rel="stylesheet" href="href://cdn.com/desktop.css" />
7 </head>
8</html>1<html>
2 <head>
3 <link
4 rel="stylesheet"
5 href="href://cdn.com/desktop.css"
6 media="print"
7 onload="this.media='all'"
8 />
9 </head>
10</html>1<link rel="preload" as="style" href="href://cdn.com/desktop.css" />Rendering diagram...
Fonts
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}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.
1@font-face {
2 font-family: 'MyFont';
3 src: url('https://cdn.com/fonts/myfont.woff2') format('woff2'),
4 font-display: swap;
5}