Skip to content

Commit 0447a70

Browse files
authored
perf: message content styles and perf scripts (#3626)
## 🎯 Goal <!-- Describe why we are making this change --> ## πŸ›  Implementation details <!-- Provide a description of the implementation --> ## 🎨 UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## πŸ§ͺ Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## β˜‘οΈ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android
1 parent 97d082d commit 0447a70

15 files changed

Lines changed: 1911 additions & 54 deletions
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Metro config that forces a `dev=false` bundle for performance profiling.
3+
*
4+
* Use this to measure scroll/render perf WITHOUT React's dev-mode wrappers
5+
* (`runWithFiberInDEV`, `getComponentStack`, etc β€” they account for ~22%
6+
* of a captured profile and don't exist in release builds). Bundle is still
7+
* unminified so function names stay readable in the .cpuprofile.
8+
*
9+
* Usage:
10+
* yarn workspace sampleapp start --config metro.config.no-dev.js --reset-cache
11+
*
12+
* Then reload the app (shake β†’ Reload, or `r` in Metro). The next bundle
13+
* fetch will be served with dev=false regardless of what the app asks for.
14+
* Run `node perf/capture-hermes-profile.js` as usual. To restore normal
15+
* dev mode just stop Metro and start it again without `--config`.
16+
*
17+
* NOTE: this only changes the served JS bundle. The native binary is still
18+
* a debug build; native code paths (Yoga, layout, view creation, image
19+
* decoding) remain debug-instrumented. To benchmark a true release native
20+
* pipeline you'd need to build a release variant of the app itself.
21+
*/
22+
const baseConfig = require('./metro.config.js');
23+
24+
module.exports = {
25+
...baseConfig,
26+
server: {
27+
...(baseConfig.server || {}),
28+
enhanceMiddleware: (middleware, metroServer) => {
29+
const wrapped =
30+
baseConfig.server && typeof baseConfig.server.enhanceMiddleware === 'function'
31+
? baseConfig.server.enhanceMiddleware(middleware, metroServer)
32+
: middleware;
33+
return (req, res, next) => {
34+
if (req.url && req.url.includes('dev=true')) {
35+
req.url = req.url.replace(/([?&])dev=true/g, '$1dev=false');
36+
// Print once-per-request so it's obvious what's happening.
37+
process.stdout.write(`[no-dev] rewrote bundle URL to: ${req.url}\n`);
38+
}
39+
return wrapped(req, res, next);
40+
};
41+
},
42+
},
43+
};

β€Žpackage/src/components/Message/MessageItemView/MessageBubble.tsxβ€Ž

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ type SwipableMessageWrapperProps = Pick<
2727
onSwipe: () => void;
2828
};
2929

30-
export const SwipableMessageWrapper = React.memo((props: SwipableMessageWrapperProps) => {
30+
export const SwipableMessageWrapper = React.memo(function SwipableMessageWrapper(
31+
props: SwipableMessageWrapperProps,
32+
) {
3133
const { children, messageSwipeToReplyHitSlop, onSwipe } = props;
3234
const { MessageSwipeContent } = useComponentsContext();
3335
const isRTL = I18nManager.isRTL;

β€Žpackage/src/components/Message/MessageItemView/MessageContent.tsxβ€Ž

Lines changed: 32 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useMemo } from 'react';
2-
import { AnimatableNumericValue, ColorValue, Pressable, StyleSheet, View } from 'react-native';
2+
import { ColorValue, Pressable, StyleSheet, View, ViewStyle } from 'react-native';
33

44
import { MessageTextContainer } from './MessageTextContainer';
55

@@ -169,47 +169,46 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
169169
[message, isMessageAIGenerated],
170170
);
171171

172-
const getBorderRadius = () => {
172+
// Merged background-color + border-radius object passed directly into the
173+
// bubble's style array (no spread at the call site). Theme-defined radii
174+
// override the group-position-computed defaults; theme-undefined radii are
175+
// omitted so they don't override the computed defaults.
176+
const bubbleColorAndRadius = useMemo<ViewStyle>(() => {
173177
// enum('top', 'middle', 'bottom', 'single')
174178
const groupPosition = groupStyles?.[0];
175-
176179
const isBottomOrSingle = groupPosition === 'single' || groupPosition === 'bottom';
177-
let borderBottomLeftRadius = components.messageBubbleRadiusGroupBottom;
178-
let borderBottomRightRadius = components.messageBubbleRadiusGroupBottom;
179180

181+
let computedBottomLeftRadius = components.messageBubbleRadiusGroupBottom;
182+
let computedBottomRightRadius = components.messageBubbleRadiusGroupBottom;
180183
if (isBottomOrSingle) {
181-
// add relevant sharp corner
184+
// add relevant sharp corner (the "tail")
182185
if (isMyMessage) {
183-
borderBottomRightRadius = components.messageBubbleRadiusTail;
186+
computedBottomRightRadius = components.messageBubbleRadiusTail;
184187
} else {
185-
borderBottomLeftRadius = components.messageBubbleRadiusTail;
188+
computedBottomLeftRadius = components.messageBubbleRadiusTail;
186189
}
187190
}
188191

189-
return {
190-
borderBottomLeftRadius,
191-
borderBottomRightRadius,
192-
};
193-
};
194-
195-
const getBorderRadiusFromTheme = () => {
196-
const bordersFromTheme: Record<string, string | AnimatableNumericValue | undefined> = {
197-
borderBottomLeftRadius,
198-
borderBottomRightRadius,
199-
borderRadius,
200-
borderTopLeftRadius,
201-
borderTopRightRadius,
192+
const style: ViewStyle = {
193+
backgroundColor,
194+
borderBottomLeftRadius: borderBottomLeftRadius ?? computedBottomLeftRadius,
195+
borderBottomRightRadius: borderBottomRightRadius ?? computedBottomRightRadius,
202196
};
197+
if (borderRadius !== undefined) style.borderRadius = borderRadius;
198+
if (borderTopLeftRadius !== undefined) style.borderTopLeftRadius = borderTopLeftRadius;
199+
if (borderTopRightRadius !== undefined) style.borderTopRightRadius = borderTopRightRadius;
203200

204-
// filter out undefined values
205-
for (const key in bordersFromTheme) {
206-
if (bordersFromTheme[key] === undefined) {
207-
delete bordersFromTheme[key];
208-
}
209-
}
210-
211-
return bordersFromTheme;
212-
};
201+
return style;
202+
}, [
203+
backgroundColor,
204+
borderBottomLeftRadius,
205+
borderBottomRightRadius,
206+
borderRadius,
207+
borderTopLeftRadius,
208+
borderTopRightRadius,
209+
groupStyles,
210+
isMyMessage,
211+
]);
213212

214213
const { setNativeScrollability } = useMessageListItemContext();
215214
const hasContentSideViews = !!(MessageContentLeadingView || MessageContentTrailingView);
@@ -357,12 +356,8 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
357356
<View
358357
style={[
359358
styles.containerInner,
360-
{
361-
backgroundColor,
362-
...getBorderRadius(),
363-
...getBorderRadiusFromTheme(),
364-
},
365-
noBorder ? { borderWidth: 0 } : {},
359+
bubbleColorAndRadius,
360+
noBorder ? styles.noBorder : null,
366361
containerInner,
367362
messageGroupedSingleOrBottom
368363
? isVeryLastMessage && enableMessageGroupingByUser
@@ -684,6 +679,7 @@ const styles = StyleSheet.create({
684679
alignSelf: 'center',
685680
},
686681
galleryContainer: {},
682+
noBorder: { borderWidth: 0 },
687683
rightAlignContent: {
688684
justifyContent: 'flex-end',
689685
},

β€Žpackage/src/components/Message/MessageItemView/MessageWrapper.tsxβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export type MessageWrapperProps = {
3030
nextMessage?: LocalMessage;
3131
};
3232

33-
export const MessageWrapper = React.memo((props: MessageWrapperProps) => {
33+
export const MessageWrapper = React.memo(function MessageWrapper(props: MessageWrapperProps) {
3434
const { message, previousMessage, nextMessage } = props;
3535
const { client } = useChatContext();
3636
const {

β€Žpackage/src/components/Message/MessageItemView/utils/renderText.tsxβ€Ž

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,37 @@ const defaultMarkdownStyles: MarkdownStyle = {
101101
fontSize: primitives.typographyFontSizeMd,
102102
lineHeight: primitives.typographyLineHeightNormal,
103103
},
104+
// Heading sizes are derived from the body font size (`typographyFontSizeMd`) so they
105+
// scale with the integrator's typography settings. lineHeight = fontSize Γ— 1.25 to
106+
// give headings room to breathe. Both fields are required here: without lineHeight,
107+
// the inherited `lineHeight: typographyLineHeightNormal` (20) from `styles.text` (set
108+
// in renderText below) leaks into the heading's inner Text via the markdown library's
109+
// text rule (`{...styles.text, ...state.style}`) and squishes larger heading fontSizes
110+
// into a 20px line box.
111+
heading1: {
112+
fontSize: primitives.typographyFontSizeMd * 2,
113+
lineHeight: primitives.typographyFontSizeMd * 2 * 1.25,
114+
},
115+
heading2: {
116+
fontSize: primitives.typographyFontSizeMd * 1.5,
117+
lineHeight: primitives.typographyFontSizeMd * 1.5 * 1.25,
118+
},
119+
heading3: {
120+
fontSize: primitives.typographyFontSizeMd * 1.25,
121+
lineHeight: primitives.typographyFontSizeMd * 1.25 * 1.25,
122+
},
123+
heading4: {
124+
fontSize: primitives.typographyFontSizeMd,
125+
lineHeight: primitives.typographyFontSizeMd * 1.25,
126+
},
127+
heading5: {
128+
fontSize: primitives.typographyFontSizeMd * 0.875,
129+
lineHeight: primitives.typographyFontSizeMd * 0.875 * 1.25,
130+
},
131+
heading6: {
132+
fontSize: primitives.typographyFontSizeMd * 0.75,
133+
lineHeight: primitives.typographyFontSizeMd * 0.75 * 1.25,
134+
},
104135
inlineCode: {
105136
padding: primitives.spacingXxs,
106137
paddingHorizontal: primitives.spacingXxs,

β€Žpackage/src/components/Message/hooks/useMessageDeliveryData.tsβ€Ž

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1-
import { useCallback, useEffect, useState } from 'react';
1+
import { useEffect, useRef, useState } from 'react';
22

33
import { Event, LocalMessage, UserResponse } from 'stream-chat';
44

55
import { useChannelContext } from '../../../contexts/channelContext/ChannelContext';
66
import { useChatContext } from '../../../contexts/chatContext/ChatContext';
7+
import { useStableCallback } from '../../../hooks';
78

89
export const useMessageDeliveredData = ({ message }: { message?: LocalMessage }) => {
910
const { channel } = useChannelContext();
1011
const { client } = useChatContext();
11-
const calculate = useCallback(() => {
12+
13+
const messageIdRef = useRef<string>(message?.id);
14+
15+
const calculate = useStableCallback(() => {
1216
if (!message?.created_at) {
1317
return [];
1418
}
@@ -17,13 +21,14 @@ export const useMessageDeliveredData = ({ message }: { message?: LocalMessage })
1721
timestampMs: new Date(message.created_at).getTime(),
1822
};
1923
return channel.messageReceiptsTracker.deliveredForMessage(messageRef);
20-
}, [channel, message]);
24+
});
2125

22-
const [deliveredTo, setDeliveredTo] = useState<UserResponse[]>([]);
26+
const [deliveredTo, setDeliveredTo] = useState<UserResponse[]>(() => calculate());
2327

24-
useEffect(() => {
28+
if (!!messageIdRef.current && !!message?.id && messageIdRef.current !== message.id) {
2529
setDeliveredTo(calculate());
26-
}, [calculate]);
30+
messageIdRef.current = message.id;
31+
}
2732

2833
useEffect(() => {
2934
const { unsubscribe } = channel.on('message.delivered', (event: Event) => {

β€Žpackage/src/components/Message/hooks/useMessageReadData.tsβ€Ž

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1-
import { useCallback, useEffect, useState } from 'react';
1+
import { useEffect, useRef, useState } from 'react';
22

33
import { Event, LocalMessage, UserResponse } from 'stream-chat';
44

55
import { useChannelContext } from '../../../contexts/channelContext/ChannelContext';
66
import { useChatContext } from '../../../contexts/chatContext/ChatContext';
7+
import { useStableCallback } from '../../../hooks';
78

89
export const useMessageReadData = ({ message }: { message?: LocalMessage }) => {
910
const { channel } = useChannelContext();
1011
const { client } = useChatContext();
11-
const calculate = useCallback(() => {
12+
13+
const messageIdRef = useRef<string>(message?.id);
14+
15+
const calculate = useStableCallback(() => {
1216
if (!message?.created_at) {
1317
return [];
1418
}
@@ -18,13 +22,14 @@ export const useMessageReadData = ({ message }: { message?: LocalMessage }) => {
1822
};
1923

2024
return channel.messageReceiptsTracker.readersForMessage(messageRef);
21-
}, [channel, message]);
25+
});
2226

23-
const [readBy, setReadBy] = useState<UserResponse[]>([]);
27+
const [readBy, setReadBy] = useState<UserResponse[]>(() => calculate());
2428

25-
useEffect(() => {
29+
if (!!messageIdRef.current && !!message?.id && messageIdRef.current !== message.id) {
2630
setReadBy(calculate());
27-
}, [calculate]);
31+
messageIdRef.current = message.id;
32+
}
2833

2934
useEffect(() => {
3035
const { unsubscribe } = channel.on('message.read', (event: Event) => {

β€Žpackage/src/components/Thread/__tests__/__snapshots__/Thread.test.tsx.snapβ€Ž

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,7 @@ exports[`Thread should match thread snapshot 1`] = `
511511
"borderBottomLeftRadius": 0,
512512
"borderBottomRightRadius": 20,
513513
},
514-
{},
514+
null,
515515
{},
516516
{},
517517
]
@@ -843,7 +843,7 @@ exports[`Thread should match thread snapshot 1`] = `
843843
"borderBottomLeftRadius": 0,
844844
"borderBottomRightRadius": 20,
845845
},
846-
{},
846+
null,
847847
{},
848848
{},
849849
]
@@ -1208,7 +1208,7 @@ exports[`Thread should match thread snapshot 1`] = `
12081208
"borderBottomLeftRadius": 0,
12091209
"borderBottomRightRadius": 20,
12101210
},
1211-
{},
1211+
null,
12121212
{},
12131213
{},
12141214
]
@@ -1534,7 +1534,7 @@ exports[`Thread should match thread snapshot 1`] = `
15341534
"borderBottomLeftRadius": 0,
15351535
"borderBottomRightRadius": 20,
15361536
},
1537-
{},
1537+
null,
15381538
{},
15391539
{},
15401540
]

β€Žperf/.gitignoreβ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
profiles/
2+
*.cpuprofile

0 commit comments

Comments
Β (0)