Large lists can drain the performance of your website. Virtualization to the rescue!
is a great solution for your React app, and now, a port of it react-window
is available on npm for your Svelte apps. Here I show, how it was ported, and how you can use it.
svelte-window
Introduction
is a smaller rewrite of react-window
with 9.8k stars at GitHub. Both libraries increase the performance of your website with virtualization, which means that they only render those parts of a large table or list, which are currently visible.
react-virtualized
I will focus on the trickier changes, which were needed to port the library, since in my last post, I covered in detail the basic steps of porting a library from React to Svelte. The ported library in that post is
. The library has only one component with a neat little helper to automatically scale up an html element to fill the space on a page. Compared to that library, react-virtualized-auto-sizer
is more complex.
react-window
I think it is a great exercise to see, where React and Svelte are similar and also where they are different. I show here what you can do when:
- the component class definition is wrapped into a function
- reactive dependency is hidden in member functions
- you need exported member functions
- setState has an additional callback
- member functions are attached to the class outside of the class definition
- (you want to get rid of memoization)(in brackets, because this case appears to be quite unique to me)
Poll: Which React library would you love to see in Svelte?
You can select more than one, or add a new entry
Anatomy of react-window
is written with static type checking via react-window
. Static type checking is great for debugging, but I don't believe that it is supported by Svelte (didn't check, actually), and I am more into typescript anyway, so I just remove all the type annotations. I pull in the types from flow
and adjust them. This way users with the typescript vscode (or neoVim etc.) plugin get all the benefits like IntelliSense, while the Svelte preprocessor is not mandatory.
definitelyTyped
There are four different exported components in react-window, a grid and a list, both as variable and fixed size variants. Internally, only one grid and one list class component is defined. There are no big differences in the implementation of the grid and the list, so I will only refer to the grid. The class definition is in
. The trick to differentiate between the fixed and variable-sized variant is that the class definition is wrapped into a function so that the inputs of the functions can be treated as external constants inside the class. It basically looks like this:
src/createGridComponent.js
/// pseudo react-window/src/createGridComponent.js ... export default function createGridComponent({ // fixed-/variable-size specific functions and props ... }{ return class Grid<T> extends PureComponent<Props<T>, State> { // React component class definition, // which treats the fixed-/variable-size specific functions and props as constants ... } } ...
This pattern can't be immediately transferred to Svelte, since the component definition has to be put into a separate
file. To achieve a similar behavior I add an extra prop named .svelte
to the inner component, which is defined in specificFunctionProps
. All functions and props are passed into this object. The export from the src/GridComponent.svelte
file is a class, which I can extend within the function and overload the constructor to inject the outer prop:
.svelte
/// svelte-window/src/createGridComponent.js import GridComponent from './GridComponent.svelte'; export default function createGridComponent( specificFunctionProps ) { return class Grid extends GridComponent { constructor(options){ options.props.specificFunctionProps = specificFunctionProps; super(options); } } }
In the script part of the
file, the functions are unloaded as GridComponent.svelte
, since they won't change:
const
/// svelte-window/src/GridComponent.svelte ... export let specificFunctionProps; const { // fixed-/variable-size specific functions and props ... } = specificFunctionProps; ...
Props, state, and render
In React, every change in props or state will cause a rerender, unless it is stopped by
member function. I Svelte, it is handled a bit differently. Take a look at the following component:
shouldComponentUpdate
<script> let n=1, a, b; const getN = () => n; $: a = getN(); $: b = getN(n+1); </script> <h1>n: {n}, a: {a}, b: {b}</h1> <button on:click={()=>n++}>click</button>
You can execute the code in REPL. When you click the button,
increases. The function n
depends on getN
. Yet, if n changes, the reactive assignment of n
is not executed, since the dependency on a
is not explicitly shown in the statement. The assignment of n
is executed, even though the function getN does not take any inputs. Other programming languages would throw an error if you don't match the expected number of inputs for a function, but javascript generously ignores the supplied b
.
n+1
In the render function of the Grid and List components, there are function calls that mask the updated variables similar to the example that I just showed. To also rerun respective reactive statement when a prop or state changes, I load the props and state into an object like this to easier track change:
let props, state; $: props = { ... } $: state = { ... }
For example, In the render function,
calls a function react-window
. It does not seem to take inputs, but in fact, inside it unloads most of the state variables and passes all props to the external callback this._getHorizontalRangeToRender()
. Thus, I wrap the whole getColumnStartIndexForOffset
code into a reactive bracket render
.
$: {...}
The
function creates an array render
and in React the children elements are directly created. This is not possible directly in Svelte. Instead, the items
array becomes a state and is filled with the data, which is necessary to render the children. The array is passed down as a slot prop. I'll show down below how this can be used.
items
In react-window you can select the tags via
and outerElementType
, so you can have a innerElementType
inside a <span>
or similar. For Svelte, there is an ongoing PR, which would easily allow this as well. As long as that is not merged, you will get a <main>
in a <div>
with <div>
. I think this is fine for 99% of use-cases, so I will not spend time implementing a workaround.
svelte-window
Exported member functions
The member functions of a React component can be called from outside the component, eg. in a parent component. In
you can use react-window
and scrollTo
. This is the example from the react-window site:
scrollToItem
import { FixedSizeList as List } from 'react-window'; const listRef = React.createRef(); // You can programmatically scroll to an item within a List. // First, attach a ref to the List: <List ref={listRef} {...props} /> // Then call the scrollToItem() API method with an item index: listRef.current.scrollToItem(200);
It doesn't seem to be covered by the Svelte tutorial(yet?), but there is a simple syntax for it. You can use
and use it similarly:
export const scrollTo = (...)=>{...}
import { FixedSizeList as List } from 'svelte-window'; let listRef; <List bind:this={listRef} {...props} /> listRef.scrollToItem(200);
Additional callback on setState
On a React component, you update the state with
and in Svelte this corresponds to a simple assignment. The setState
function also takes a callback, which is called after the state has been updated and setState
uses this. Eg. in the react-window
member function there is the following code:
scrollTo
this.setState(prevState=>..., this._resetIsScrollingDebounced);
In the ported code, I add a state variable
, which is set to request_resetIsScrollingDebounced
. When the code is reached, that corresponds to the false
statement, I set it to setState
. Then I use Sveltes true
lifecycle function:
afterUpdate
import {afterUpdate} from 'svelte'; let request_resetOsScrollingDebounded = false; export const scrollTo = ... afterUpdate(()=>{ if (request_resetIsScrollingDebounced){ _resetIsScrollingDebounced(); } });
React style objects
A good thing about React is that you can create inline styles with nice style objects, while in Svelte you can only pass a string to the style attribute. An important part of
is to pass a style object down to the children. I decided to also pass the styles as an object and provide a simple parser as additional export of react-window
, namely the function svelte-window
. This generates some overhead for the user. The rationale is that you can more easily work with objects, eg. if you want to perform cell-merging, you don't have to parse the string back. You can use this small function, which only handles styles that are injected by styleString
. You could also import more elaborate functions, eg. svelte-window
.
react-style-object-to-css
Additional exported member functions
For the variable-sized list and grid, additional methods are added outside of the class definition in
.In src/createGridComponent.js
the callback src/VariableSizeGrid.js
the keyword initInstanceProps
is passed in as this
so that additional member functions can be added to the class. This makes sure, only the variable-sized varieties get these. The methods are instance
/resetAfterIndex
, resetAfterIndices
and resetAfterColumnIndex
. According to the documentation, these should be called when the row/column heights/widths change, which is typically not the case.
resetAfterRowIndex
An identical implementation is not possible, since
is not referring to the class in a this
file. It is .svelte
. The way I implemented it is to add an exported object undefined
:
instance
export const instance = {_getItemStyleCache:_getItemStyleCache};
This object is passed to
instead of initInstanceProps
. As a consequence, the functions are attached to the this
member instead of the class itself. So, eg. a call in React which looks like this:
instance
myGrid.resetAfterColumnIndex(42);
would look like this in Svelte:
myGrid.instance.resetAfterColumnIndex(42);
Hope it is not causing too much inconvenience.
Remove memoize-one
These have been all the important steps that are needed to port
. At this point, I was able to test it and it works well. It has only one external dependency left to react-window
. It is not a big library, so I could leave it in. In general, memoization is a technique to improve the performance of an app. It means that you save the inputs and the outputs of an expensive function, and when you need to re-run the function with the same inputs, you just use the saved output instead of re-running. It is a common case in React, that functions are executed eg. on each render, even though nothing changed. memoize-one
is a library that only keeps track of the last run of the expensive function.
memoize-one
When you port from React to Svelte, it makes sense to look at the memoized functions. In general, React defaults to re-execute functions due to the functional paradigm, while Svelte defaults to only execute functions if something changes, called reactivity. In this case, I think the case is different, because, to me, memoization was used in an unorthodox manner. Let's take a look.
Case one and two: onItemsRendered and onScroll
and onItemsRendered
are props. You can supply a callback here, and when items are rendered or scrolling happened, these are fired. They are wrapped via onScroll
in memoize-one
and _callOnItemsRendered
and inside the function _callOnScroll
it is checked if the callbacks should be fired. It is not important if the callbacks are expensive and the return value is ignored. Memoization here is just a shorthand to check if the inputs changed. In total there are 7 variables for the list and 13 for the grid which is checked, so I add cache objects for both cases and check for differences manually. It is just a little more to write, but we can get rid of an external dependency quite easily.
_callPropsCallbacks
Case three: _getItemStyleCache
Remember that memoizations typical use is to wrap expensive functions? In this case, a dummy function is wrapped:
const _getItemStyleCache = memoizeOne((_, __, ___) => ({}));
When you call this function with new inputs, it returns an empty object. This object is used to cache styles and when you call
with the same inputs, it will return the object with the styles, not an empty one. That is another funky use-case of _getItemStyleCache
. I exchange that with this:
memoize-one
let _styleCache = {},_styleCacheCheck={}; const _getItemStyleCache = (a,b,c)=>{ if ( a === _styleCacheCheck.a && b === _styleCacheCheck.b && c === _styleCacheCheck.c ){ return _styleCache; } _styleCacheCheck = {a,b,c} _styleCache = {}; return _styleCache; }
Instead of two lines, there are 14 lines now, but it means that we can completely get rid of an external dependency.
How to use svelte-window
I noted before, that the children of the list cannot be rendered directly as in
. Instead, the item information is passed down as a slot prop, and the children have to be rendered explicitly. So, if we take the first example from the react-window
site:
react-window
import { FixedSizeList as List } from 'react-window'; const Row = ({ index, style }) => ( <div style={style}>Row {index}</div> ); const Example = () => ( <List height={150} itemCount={1000} itemSize={35} width={300} > {Row} </List> );
it would look like this in Svelte:
<script> import { FixedSizeList as List, styleString as sty } from 'svelte-window'; </script> <List height={150} itemCount={1000} itemSize={35} width={300} let:items> {#each items as it (it.key)} <div style={sty(it.style)}>Row {it.index}</div> {/each} </List>
Here is another example with a fixed size grid with a scroll-to button, scrolling indicators:
<script> import { FixedSizeGrid as Grid , styleString as sty} from 'svelte-window'; let grid; const click = () => { if (grid){ grid.scrollToItem({ align: "start", columnIndex: 150, rowIndex: 300 }); } } </script> <Grid bind:this={grid} columnCount={1000} columnWidth={100} height={150} rowCount={1000} rowHeight={35} width={300} useIsScrolling let:items > {#each items as it (it.key)} <div style={sty(it.style)}> { isScrolling ? 'Scrolling' : `Row ${rowIndex} - Col ${columnIndex}` } <div> {/each} </Grid> <button on:click={click}> To row 300, column 150 </button>
With these two examples, it should be easy enough to replicate the examples from https://react-window.now.sh/.
Summary
With
I ported a flexible, high-quality React library to Svelte. There were few things, which are implemented differently, but after all the process is more or less the same. It will be interesting to see, how this library performs in real-world situations. Github currently lists 19k projects which depend on this library, so I think it might be a good addition to the Svelte eco-system.
react-window
The syntax is slightly different, the user needs to do a little more than with the React counterpart. I think that there is an upside to it since you have a little more control over the process. Especially it should be possible to merge cells and rows more easily. I'll go more into detail in an upcoming post.
I hope you enjoy this library. After all, porting was not a lot of work. Many thanks to the authors of this library, especially Brian Vaughn!
Share on Twitter - Discuss on Reddit - Vote on next topics - Support me
This might also be interesting for you:
- How to Implement Custom React Hooks in Svelte
- React Hooks in Svelte
- tree-shake-css - project page
- Sveltes tick is my new best friend - porting react-textfit to Svelte
- How to merge cells with svelte-window
- Poll: Which React library would you love to see in Svelte?
- Porting React components to Svelte - It might be easier than you think
- Svelte and Typescript
- How this blog is made - Basic Structure, Elder and Markdown
- How this blog is made part 2 - Make your website sketchy with Tailwind and Roughjs
- Showcase: The Descent Ripple Effect or The React Descent Ripple Effect
- Why are component libraries so complicated with React compared to Svelte?