SolitaireCat.com is a cat-themed Solitaire playing website and Solitaire history blog, because, why not?
I developed the site as a project to improve my Typescript skills and get some exposure to SolidJS and Supabase. A game seemed like a fun enterprise to achieve these goals, and online Solitaire feels like the yardstick of the genre.
Design goals and game features
Game
- Move hinting
- Undo / redo
Visuals
- HTML / SVG for all visual elements - i.e. all vector
- Smooth cat-themed animations for card movement, hinting, drop, invalid moves, win celebration etc.
Accounts/backend
- User accounts with usernames, email, password resets etc. all in game.
- Player profiles with ranks, best games etc.
- User game preferences
- Load / save games
- High scores
Canvas vs. DOM
The game itself runs entirely on the client and is written in vanilla Typescript
and CSS
.
I don’t have any experience using Canvas, but my base assumption was that a game like this is far more easily implemented using the DOM. The DOM gives us animations and rendering for free. Flipping and moving cards around is as simple as applying the appropriate CSS transformations. This may not be true for an experienced Canvas developer or if using a framework, but my DOM/CSS:Canvas knowledge ratio is ∞.
It’s not a demanding game and SVG/CSS animation is plenty performant.
Minimizing the size of the SVG cards
A full deck of dynamically generated SVG cards in 47 KB.
The SVG cards are generated dynamically from a set of SVG primitives. See here for a separate article here detailing the process.
SVG animation
Beyond the cards, there are various animations included, such as the cat advisor shown below. By using SMIL animation to animate the paths and transforms, smooth animations can be produced without adding too much to the page weight.
Technology stack
The Solitaire game itself runs entirely on the client and is written in vanilla Typescript
.
The back-end (user accounts, saved games, etc.) is hosted on Supabase
, with all data stored in PostgreSQL and Supabase edge functions
used to validate game results and perform some account maintenance tasks.
SolidJS
is used on the client to manage user and user preference state, and to render the various modal windows (e.g. leaderboards, saved games, user profiles, etc.).
For the accompanying blog, it’s generated by Hugo and uses the same CSS framework that I use for this blog, although it’s not much of a framework, just a few SCSS files that I use as the base for most client-side web projects.
It’s hosted on Cloudflare Pages and Cloudflare Workers are used to send email from the contact us form.
Reducing the page weight
The only runtime dependencies of the app are SolidJS and SupabaseJS. Solid is very efficient, supports tree-shaking, and only adds ~7 KB
to the app. Supabase on the other hand…
Supabase doesn’t support tree shaking
Supabase is implemented using classes, and so mostly doesn’t support tree shaking. The Javascript client includes a lot of functionality that I don’t use. Things like the realtime API, storage API, and a fetch implementation.
Version 2.39
breaks down as follows:
Component | gzip |
---|---|
gotrue | 25 KB |
realtime | 15 KB |
postgrest | 10 KB |
storage | 7 KB |
supabase | 5 KB |
functions | 2 KB |
node-fetch | 1 KB |
total | 65KB |
Bundle breakdown for SupabaseJS client v2.39
A simple program that uses the Supabase JS client with no method calls, like the following:
export { createClient } from "@supabase/[email protected]";
const client = createClient("url", "key");
Results in a gzip bundle size of 25 KB
.
To reduce this, I forked supabase-js, gotrue-js, functions-js and postgrest-js and did the following:
- Removed realtime/storage imports/exports.
- Removed realtime/storage methods on the client.
- Removed fallback fetch / custom fetch capability.
This reduced the gzip size from 25 KB
to 14 KB
. (raw 96 KB
-> 54 KB
).
The final JS size is 50 KB (gzip)
I estimate the SolidJS runtime is ~7 KB
, and my game code / card gen is 29KB
:
Sizes (KB) | gzip |
---|---|
Supabase | 14 KB |
SolidJS | 7?KB |
Game | 29 KB |
Total | 50 KB |
Inlining SVG symbols into HTML
The game uses various small SVG elements, such as crossed paw effects, cat head popcorn, menu symbols etc.
There are some arguments for keeping these as separate files/resources for caching, easier maintainence etc., but: one 5 KB request is better than five 1 KB requests, the HTML is cached anyway, won’t change often, and is the only page using them, they can be kept separate and inlined on build anyway, and making all the preceding points moot, they need to be inlined regardless in order for gradients to work (see here and here).
Vite provides the transformIndexHtml
in its plug-in API, which in conjunction with Node’s file system API, can easily be made to inline the SVG into the index.html
document.
All of the SVG content is inlined, with the exception of the card backs, since most go unused.
The gzipped and minified HTML file after SVG inlining is 19 KB
.
The final game is 148 KB
all in
Sizes (KB) | gzip |
---|---|
SVG cards | 46 KB |
HTML | 19 KB |
CSS | 8 KB |
images* | 25 KB |
JS | 50 KB |
Total | 148 KB |
* Card backs and table felt. Other SVG images are inlined on the HTML.
Theres always room to reduce further, but the marginal returns by that point didn’t warrant it.
That is all
So that’s how SolitaireCat.com came about. Please take a look, play a hand, and let me know if anything is not working as it should be.
Appendix: Stack summary
Front-end
-
Vanilla Typescript
Gameplay, animations. -
SolidJS
User and user preference state, rendering the various modal windows (e.g. leaderboard, user profile, etc.). -
SupabaseJS
JS interface to Supabase Postgres / Edge functions backend.
Back-end
-
Supabase edge functions (Typescript)
Validation of game data, account maintenance tasks. -
Supabase Postgres (PL/pgSQL)
User account storage, user profiles, saved games, leaderboards. -
Cloudflare workers (Typescript)
Contact us email submissions. -
Cloudflare pages
Static hosting.