Die Anwendungsleistung ist ein breites und komplexes Feld, und während Solid Query Ihre APIs nicht schneller machen kann, gibt es dennoch Dinge, die Sie bei der Verwendung von Solid Query beachten sollten, um die beste Leistung zu gewährleisten.
Das größte Performance-Problem bei der Verwendung von Solid Query oder jeder anderen Datenabrufbibliothek, die es Ihnen erlaubt, Daten innerhalb von Komponenten abzurufen, sind Request Waterfalls. Der Rest dieser Seite erklärt, was sie sind, wie Sie sie erkennen und wie Sie Ihre Anwendung oder APIs umstrukturieren können, um sie zu vermeiden.
Der Leitfaden Prefetching & Router Integration baut darauf auf und lehrt Sie, wie Sie Daten im Voraus vorab abrufen können, wenn es nicht möglich oder machbar ist, Ihre Anwendung oder APIs umzustrukturieren.
Der Leitfaden Server Rendering & Hydration lehrt Sie, wie Sie Daten auf dem Server vorab abrufen und diese Daten an den Client weitergeben können, damit Sie sie nicht erneut abrufen müssen.
Der Leitfaden Advanced Server Rendering lehrt Sie weiter, wie Sie diese Muster auf Server Components und Streaming Server Rendering anwenden.
Ein Request Waterfall entsteht, wenn eine Anfrage nach einer Ressource (Code, CSS, Bilder, Daten) erst *nachdem* eine andere Anfrage nach einer Ressource abgeschlossen ist, beginnt.
Betrachten Sie eine Webseite. Bevor Sie Dinge wie CSS, JS usw. laden können, muss der Browser zuerst das Markup laden. Dies ist ein Request Waterfall.
1. |-> Markup
2. |-> CSS
2. |-> JS
2. |-> Image
1. |-> Markup
2. |-> CSS
2. |-> JS
2. |-> Image
Wenn Sie Ihr CSS innerhalb einer JS-Datei abrufen, haben Sie jetzt einen doppelten Waterfall.
1. |-> Markup
2. |-> JS
3. |-> CSS
1. |-> Markup
2. |-> JS
3. |-> CSS
Wenn dieses CSS ein Hintergrundbild verwendet, ist es ein dreifacher Waterfall.
1. |-> Markup
2. |-> JS
3. |-> CSS
4. |-> Image
1. |-> Markup
2. |-> JS
3. |-> CSS
4. |-> Image
Der beste Weg, Ihre Request Waterfalls zu erkennen und zu analysieren, ist normalerweise, den "Netzwerk"-Tab in den Entwicklertools Ihres Browsers zu öffnen.
Jeder Waterfall repräsentiert mindestens einen Roundtrip zum Server, es sei denn, die Ressource ist lokal zwischengespeichert (in der Praxis können einige dieser Waterfalls mehr als einen Roundtrip darstellen, da der Browser eine Verbindung herstellen muss, was etwas Hin und Her erfordert, aber wir ignorieren das hier). Daher hängen die negativen Auswirkungen von Request Waterfalls stark von der Latenz des Benutzers ab. Betrachten Sie das Beispiel des dreifachen Waterfalls, der tatsächlich 4 Server-Roundtrips darstellt. Bei einer Latenz von 250 ms, was auf 3G-Netzwerken oder bei schlechten Netzwerkbedingungen nicht ungewöhnlich ist, erhalten wir eine Gesamtzeit von 4 * 250 = 1000 ms, **nur die Latenz gezählt**. Wenn wir dies in das erste Beispiel mit nur 2 Roundtrips abflachen könnten, erhalten wir stattdessen 500 ms, wodurch das Hintergrundbild möglicherweise in der halben Zeit geladen wird!
Betrachten wir nun Solid Query. Wir konzentrieren uns zuerst auf den Fall ohne Server Rendering. Bevor wir überhaupt mit dem Abfragen beginnen können, müssen wir das JS laden, also bevor wir diese Daten auf dem Bildschirm anzeigen können, haben wir einen doppelten Waterfall.
1. |-> Markup
2. |-> JS
3. |-> Query
1. |-> Markup
2. |-> JS
3. |-> Query
Auf dieser Grundlage betrachten wir nun einige verschiedene Muster, die zu Request Waterfalls in Solid Query führen können, und wie man sie vermeidet.
Wenn eine einzelne Komponente zuerst eine Abfrage und dann eine weitere abruft, ist das ein Request Waterfall. Dies kann passieren, wenn die zweite Abfrage eine abhängige Abfrage ist, d.h. sie hängt beim Abrufen von Daten aus der ersten Abfrage ab.
// Get the user
const { data: user } = useQuery(() => {
queryKey: ['user', email],
queryFn: getUserByEmail,
})
const userId = user?.id
// Then get the user's projects
const {
status,
fetchStatus,
data: projects,
} = useQuery(() => {
queryKey: ['projects', userId],
queryFn: getProjectsByUser,
// The query will not execute until the userId exists
enabled: !!userId,
})
// Get the user
const { data: user } = useQuery(() => {
queryKey: ['user', email],
queryFn: getUserByEmail,
})
const userId = user?.id
// Then get the user's projects
const {
status,
fetchStatus,
data: projects,
} = useQuery(() => {
queryKey: ['projects', userId],
queryFn: getProjectsByUser,
// The query will not execute until the userId exists
enabled: !!userId,
})
Obwohl nicht immer machbar, ist es für optimale Leistung besser, Ihre API so zu restrukturieren, dass Sie beide in einer einzigen Abfrage abrufen können. Im obigen Beispiel, anstatt zuerst getUserByEmail abzurufen, um getProjectsByUser abrufen zu können, würde die Einführung einer neuen Abfrage getProjectsByUserEmail den Waterfall abflachen.
Eine weitere Möglichkeit, abhängige Abfragen zu mindern, ohne Ihre API zu restrukturieren, ist, den Waterfall auf den Server zu verlagern, wo die Latenz geringer ist. Dies ist die Idee hinter Server Components, die im Leitfaden Advanced Server Rendering behandelt werden.
Ein weiteres Beispiel für serielle Abfragen ist, wenn Sie Solid Query mit Suspense verwenden.
function App () {
// The following queries will execute in serial, causing separate roundtrips to the server:
const usersQuery = useSuspenseQuery({ queryKey: ['users'], queryFn: fetchUsers })
const teamsQuery = useSuspenseQuery({ queryKey: ['teams'], queryFn: fetchTeams })
const projectsQuery = useSuspenseQuery({ queryKey: ['projects'], queryFn: fetchProjects })
// Note that since the queries above suspend rendering, no data
// gets rendered until all of the queries finished
...
}
function App () {
// The following queries will execute in serial, causing separate roundtrips to the server:
const usersQuery = useSuspenseQuery({ queryKey: ['users'], queryFn: fetchUsers })
const teamsQuery = useSuspenseQuery({ queryKey: ['teams'], queryFn: fetchTeams })
const projectsQuery = useSuspenseQuery({ queryKey: ['projects'], queryFn: fetchProjects })
// Note that since the queries above suspend rendering, no data
// gets rendered until all of the queries finished
...
}
Beachten Sie, dass bei regulärem useQuery diese parallel ablaufen würden.
Glücklicherweise ist dies leicht zu beheben, indem Sie immer den Hook useSuspenseQueries verwenden, wenn Sie mehrere suspendierende Abfragen in einer Komponente haben.
const [usersQuery, teamsQuery, projectsQuery] = useSuspenseQueries({
queries: [
{ queryKey: ['users'], queryFn: fetchUsers },
{ queryKey: ['teams'], queryFn: fetchTeams },
{ queryKey: ['projects'], queryFn: fetchProjects },
],
})
const [usersQuery, teamsQuery, projectsQuery] = useSuspenseQueries({
queries: [
{ queryKey: ['users'], queryFn: fetchUsers },
{ queryKey: ['teams'], queryFn: fetchTeams },
{ queryKey: ['projects'], queryFn: fetchProjects },
],
})
Verschachtelte Komponenten-Waterfalls entstehen, wenn sowohl eine Eltern- als auch eine Kindkomponente Abfragen enthalten und die Eltern die Kindkomponente nicht rendert, bis ihre Abfrage abgeschlossen ist. Dies kann sowohl mit useQuery als auch mit useSuspenseQuery passieren.
Wenn das Kind bedingt auf den Daten der Eltern gerendert wird oder wenn das Kind auf einen Teil des Ergebnisses angewiesen ist, der als Prop von der Elternkomponente an die eigene Abfrage weitergegeben wird, haben wir einen *abhängigen* verschachtelten Komponenten-Waterfall.
Betrachten wir zuerst ein Beispiel, bei dem das Kind **nicht** von der Elternkomponente abhängig ist.
function Article({ id }) {
const { data: articleData, isPending } = useQuery(() => {
queryKey: ['article', id],
queryFn: getArticleById,
})
if (isPending) {
return 'Loading article...'
}
return (
<>
<ArticleHeader articleData={articleData} />
<ArticleBody articleData={articleData} />
<Comments id={id} />
</>
)
}
function Comments({ id }) {
const { data, isPending } = useQuery(() => {
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
...
}
function Article({ id }) {
const { data: articleData, isPending } = useQuery(() => {
queryKey: ['article', id],
queryFn: getArticleById,
})
if (isPending) {
return 'Loading article...'
}
return (
<>
<ArticleHeader articleData={articleData} />
<ArticleBody articleData={articleData} />
<Comments id={id} />
</>
)
}
function Comments({ id }) {
const { data, isPending } = useQuery(() => {
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
...
}
Beachten Sie, dass <Comments> zwar eine Prop id von der Elternkomponente nimmt, diese ID aber bereits verfügbar ist, wenn die <Article> gerendert wird, sodass es keinen Grund gibt, die Kommentare nicht gleichzeitig mit dem Artikel abzurufen. In realen Anwendungen kann die Kindkomponente weit unter der Elternkomponente verschachtelt sein, und diese Arten von Waterfalls sind oft kniffliger zu erkennen und zu beheben. Für unser Beispiel wäre eine Möglichkeit, den Waterfall abzuflachen, die Kommentare-Abfrage in die Elternkomponente zu verschieben.
function Article({ id }) {
const { data: articleData, isPending: articlePending } = useQuery(() => {
queryKey: ['article', id],
queryFn: getArticleById,
})
const { data: commentsData, isPending: commentsPending } = useQuery(() => {
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
if (articlePending) {
return 'Loading article...'
}
return (
<>
<ArticleHeader articleData={articleData} />
<ArticleBody articleData={articleData} />
{commentsPending ? (
'Loading comments...'
) : (
<Comments commentsData={commentsData} />
)}
</>
)
}
function Article({ id }) {
const { data: articleData, isPending: articlePending } = useQuery(() => {
queryKey: ['article', id],
queryFn: getArticleById,
})
const { data: commentsData, isPending: commentsPending } = useQuery(() => {
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
if (articlePending) {
return 'Loading article...'
}
return (
<>
<ArticleHeader articleData={articleData} />
<ArticleBody articleData={articleData} />
{commentsPending ? (
'Loading comments...'
) : (
<Comments commentsData={commentsData} />
)}
</>
)
}
Die beiden Abfragen werden nun parallel abgerufen. Beachten Sie, dass Sie bei Verwendung von Suspense diese beiden Abfragen in einer einzigen useSuspenseQueries zusammenfassen möchten.
Eine weitere Möglichkeit, diesen Waterfall abzuflachen, wäre, die Kommentare in der <Article> Komponente vorab abzurufen oder beide Abfragen auf Router-Ebene beim Laden der Seite oder bei der Seitennavigation vorab abzurufen. Lesen Sie mehr darüber im Leitfaden Prefetching & Router Integration.
Als Nächstes betrachten wir einen *Abhängigen Verschachtelten Komponenten-Waterfall*.
function Feed() {
const { data, isPending } = useQuery(() => {
queryKey: ['feed'],
queryFn: getFeed,
})
if (isPending) {
return 'Loading feed...'
}
return (
<>
{data.map((feedItem) => {
if (feedItem.type === 'GRAPH') {
return <GraphFeedItem key={feedItem.id} feedItem={feedItem} />
}
return <StandardFeedItem key={feedItem.id} feedItem={feedItem} />
})}
</>
)
}
function GraphFeedItem({ feedItem }) {
const { data, isPending } = useQuery(() => {
queryKey: ['graph', feedItem.id],
queryFn: getGraphDataById,
})
...
}
function Feed() {
const { data, isPending } = useQuery(() => {
queryKey: ['feed'],
queryFn: getFeed,
})
if (isPending) {
return 'Loading feed...'
}
return (
<>
{data.map((feedItem) => {
if (feedItem.type === 'GRAPH') {
return <GraphFeedItem key={feedItem.id} feedItem={feedItem} />
}
return <StandardFeedItem key={feedItem.id} feedItem={feedItem} />
})}
</>
)
}
function GraphFeedItem({ feedItem }) {
const { data, isPending } = useQuery(() => {
queryKey: ['graph', feedItem.id],
queryFn: getGraphDataById,
})
...
}
Die zweite Abfrage getGraphDataById hängt auf zwei verschiedene Arten von ihrer Elternkomponente ab. Erstens passiert sie nie, es sei denn, das feedItem ist ein Graph, und zweitens benötigt sie eine id von der Elternkomponente.
1. |> getFeed()
2. |> getGraphDataById()
1. |> getFeed()
2. |> getGraphDataById()
In diesem Beispiel können wir den Waterfall nicht einfach durch Verschieben der Abfrage zur Elternkomponente oder durch Hinzufügen von Prefetching abflachen. Ähnlich wie beim Beispiel der abhängigen Abfrage am Anfang dieses Leitfadens ist eine Option, unsere API zu refaktorisieren, um die Graphdaten in die getFeed Abfrage aufzunehmen. Eine weitere fortgeschrittene Lösung ist die Nutzung von Server Components, um den Waterfall auf den Server zu verlagern, wo die Latenz geringer ist (lesen Sie mehr darüber im Leitfaden Advanced Server Rendering), aber beachten Sie, dass dies eine sehr große architektonische Änderung sein kann.
Sie können auch mit ein paar Abfrage-Waterfalls hier und da eine gute Leistung erzielen. Wissen Sie einfach, dass sie ein häufiges Leistungsproblem darstellen und seien Sie sich dessen bewusst. Eine besonders heimtückische Variante ist, wenn Code Splitting involviert ist, schauen wir uns das als Nächstes an.
Das Aufteilen des JS-Codes einer Anwendung in kleinere Blöcke und das Laden nur der notwendigen Teile ist normalerweise ein wichtiger Schritt zur Erzielung guter Leistung. Es hat jedoch einen Nachteil, nämlich dass es oft Request Waterfalls einführt. Wenn dieser code-gesplittete Code auch eine Abfrage enthält, verschlimmert sich dieses Problem noch.
Betrachten Sie dies als eine leicht modifizierte Version des Feed-Beispiels.
// This lazy loads the GraphFeedItem component, meaning
// it wont start loading until something renders it
const GraphFeedItem = Solid.lazy(() => import('./GraphFeedItem'))
function Feed() {
const { data, isPending } = useQuery(() => {
queryKey: ['feed'],
queryFn: getFeed,
})
if (isPending) {
return 'Loading feed...'
}
return (
<>
{data.map((feedItem) => {
if (feedItem.type === 'GRAPH') {
return <GraphFeedItem key={feedItem.id} feedItem={feedItem} />
}
return <StandardFeedItem key={feedItem.id} feedItem={feedItem} />
})}
</>
)
}
// GraphFeedItem.tsx
function GraphFeedItem({ feedItem }) {
const { data, isPending } = useQuery(() => {
queryKey: ['graph', feedItem.id],
queryFn: getGraphDataById,
})
...
}
// This lazy loads the GraphFeedItem component, meaning
// it wont start loading until something renders it
const GraphFeedItem = Solid.lazy(() => import('./GraphFeedItem'))
function Feed() {
const { data, isPending } = useQuery(() => {
queryKey: ['feed'],
queryFn: getFeed,
})
if (isPending) {
return 'Loading feed...'
}
return (
<>
{data.map((feedItem) => {
if (feedItem.type === 'GRAPH') {
return <GraphFeedItem key={feedItem.id} feedItem={feedItem} />
}
return <StandardFeedItem key={feedItem.id} feedItem={feedItem} />
})}
</>
)
}
// GraphFeedItem.tsx
function GraphFeedItem({ feedItem }) {
const { data, isPending } = useQuery(() => {
queryKey: ['graph', feedItem.id],
queryFn: getGraphDataById,
})
...
}
Dieses Beispiel hat einen doppelten Waterfall, der so aussieht:
1. |> getFeed()
2. |> JS for <GraphFeedItem>
3. |> getGraphDataById()
1. |> getFeed()
2. |> JS for <GraphFeedItem>
3. |> getGraphDataById()
Aber das ist nur die Betrachtung des Codes aus dem Beispiel. Wenn wir betrachten, wie der erste Seitenaufruf dieser Seite aussieht, müssen wir tatsächlich 5 Roundtrips zum Server abschließen, bevor wir den Graphen rendern können!
1. |> Markup
2. |> JS for <Feed>
3. |> getFeed()
4. |> JS for <GraphFeedItem>
5. |> getGraphDataById()
1. |> Markup
2. |> JS for <Feed>
3. |> getFeed()
4. |> JS for <GraphFeedItem>
5. |> getGraphDataById()
Beachten Sie, dass dies beim Server Rendering etwas anders aussieht. Wir werden dies im Leitfaden Server Rendering & Hydration weiter untersuchen. Beachten Sie auch, dass es nicht ungewöhnlich ist, dass die Route, die <Feed> enthält, ebenfalls code-gesplittet ist, was einen weiteren Hop hinzufügen könnte.
Im code-gesplitteten Fall kann es tatsächlich hilfreich sein, die Abfrage getGraphDataById in die <Feed> Komponente zu verschieben und sie bedingt zu machen oder ein bedingtes Prefetching hinzuzufügen. Diese Abfrage könnte dann parallel zum Code abgerufen werden, wodurch das Beispiel so aussieht:
1. |> getFeed()
2. |> getGraphDataById()
2. |> JS for <GraphFeedItem>
1. |> getFeed()
2. |> getGraphDataById()
2. |> JS for <GraphFeedItem>
Dies ist jedoch ein Kompromiss. Sie binden jetzt den Datenabrufcode für getGraphDataById in denselben Bundle wie <Feed> ein. Bewerten Sie also, was für Ihren Fall am besten ist. Lesen Sie mehr darüber, wie Sie dies tun können, im Leitfaden Prefetching & Router Integration.
Der Kompromiss zwischen
- Alle Datenabruf-Codes in das Hauptbundle aufnehmen, auch wenn wir sie selten verwenden
- Den Datenabruf-Code in das code-gesplittete Bundle legen, aber mit einem Request Waterfall
ist nicht ideal und war eine der Motivationen für Server Components. Mit Server Components ist es möglich, beides zu vermeiden. Lesen Sie mehr darüber, wie sich dies auf Solid Query auswirkt, im Leitfaden Advanced Server Rendering.
Request Waterfalls sind ein sehr häufiges und komplexes Performance-Problem mit vielen Kompromissen. Es gibt viele Möglichkeiten, sie versehentlich in Ihre Anwendung einzuführen.
Aufgrund dieser unbeabsichtigten Komplexität lohnt es sich, auf Waterfalls zu achten und Ihre Anwendung regelmäßig daraufhin zu untersuchen (ein guter Weg ist, ab und zu den Netzwerk-Tab zu untersuchen!). Sie müssen nicht unbedingt alle abflachen, um eine gute Leistung zu erzielen, aber behalten Sie die mit hoher Auswirkung im Auge.
Im nächsten Leitfaden werden wir weitere Möglichkeiten zum Abflachen von Waterfalls untersuchen, indem wir Prefetching & Router Integration nutzen.