# Mobile-First Web Performance Review — darcsweb
## Summary
The project already has a viewport meta tag and some responsive CSS hooks, so this is not starting from a desktop-only baseline. The biggest problems are server-side: the repository summary and patch-detail paths do much more darcs work than their UI implies, so TTFB will scale poorly with repository count and history size. On mobile, the Haskell-generated HTML still leans on desktop tables and generic `div` wrappers, which leaves narrow-screen views under-labeled and less semantic than they should be.
## Mobile rendering issues
- The repo list, shortlog, tags, and tree pages emit plain tables with header-only labels. Once the existing stylesheet collapses those tables for small screens, the Haskell markup provides no inline labels, `caption`, or `data-label` fallback, so date/author/size values lose context on a 360px phone. `src/DarcsWeb/Html.hs:68`, `src/DarcsWeb/Html.hs:154`, `src/DarcsWeb/Html.hs:262`, `src/DarcsWeb/Html.hs:312`
- The summary page’s only “see more” affordance for recent activity is the literal text `...`, which is both a weak tap target and easy to miss on touch devices. Mobile-first navigation needs a full-label control here. `src/DarcsWeb/Html.hs:109`
- Blob and diff pages always render raw `<pre>` blocks for file contents and diffs. That preserves fidelity, but it also guarantees horizontal panning for long lines instead of offering a mobile-readable fallback. `src/DarcsWeb/Html.hs:241`, `src/DarcsWeb/Html.hs:361`
## Performance issues
- The repository summary route does three expensive metadata walks on every request: it rescans all repositories to rediscover the current repo, then rereads the target repository once for recent patches and once again for tags. On a host with many repositories or long histories, `/repo/:name/summary` TTFB will be dominated by repeated `listRepos` and `readPatches` work. `app/Main.hs:238`, `app/Main.hs:245`, `app/Main.hs:246`, `src/DarcsWeb/Darcs.hs:63`, `src/DarcsWeb/Darcs.hs:108`, `src/DarcsWeb/Darcs.hs:123`, `src/DarcsWeb/Darcs.hs:155`
- `getRepoPatch` computes full diff text before it knows it has found the requested patch hash. The current `mapRL extractPatchFull` plus `filter` shape can render large diffs for many non-matching patches just to serve one detail page. `src/DarcsWeb/Darcs.hs:136`, `src/DarcsWeb/Darcs.hs:139`, `src/DarcsWeb/Darcs.hs:263`
- HTML escaping is on the hot path and still goes through `Text -> String -> Text` conversion. `highlightDiff` then repeats that work per line, so the largest pages pay the highest avoidable allocation cost. `src/DarcsWeb/Html.hs:413`, `src/DarcsWeb/Html.hs:418`, `gen/HtmlPure.hs:37`
- Blob and repository-description reads still use lazy `readFile` into `String` and then `T.pack`, which duplicates content in memory and defers I/O/exception timing. That is directly on the response path for blob views and indirectly on the index/summary pages. `src/DarcsWeb/Darcs.hs:103`, `src/DarcsWeb/Darcs.hs:249`
- Clone traffic is forced through `Cache-Control: no-cache` for every `_darcs` object. That prevents clients and intermediaries from reusing hashed immutable clone artifacts efficiently, so repeat clones and resumed downloads pay extra RTTs. `app/Main.hs:434`, `app/Main.hs:435`
## Design & semantics
- Page chrome and primary content are built from generic `div` wrappers instead of semantic landmarks such as `<header>`, `<main>`, `<footer>`, and full-log entries are not `<article>` elements. That weakens landmark navigation and makes the HTML structure less coherent than the visual design suggests. `src/DarcsWeb/Html.hs:39`, `src/DarcsWeb/Html.hs:45`, `src/DarcsWeb/Html.hs:48`, `src/DarcsWeb/Html.hs:195`
- The repository index has no visible `<h1>` at all; it jumps straight from page chrome to a table or an empty-state paragraph. Screen-reader and keyboard users lose the page’s primary heading, and narrow-screen users get less orientation than they should. `src/DarcsWeb/Html.hs:56`, `src/DarcsWeb/Html.hs:60`, `src/DarcsWeb/Html.hs:68`
- Several tables ship unlabeled structural columns via empty `<th>` cells. That creates blank headers for assistive tech and makes the mobile-collapsed table views harder to interpret. `src/DarcsWeb/Html.hs:72`, `src/DarcsWeb/Html.hs:156`, `src/DarcsWeb/Html.hs:263`, `src/DarcsWeb/Html.hs:313`
- Author presentation is inconsistent across pages: shortlog and tags collapse to the display name, while full log and patch detail render the raw author field. That creates avoidable visual inconsistency and increases the chance of long-address overflow on narrow layouts. `src/DarcsWeb/Html.hs:166`, `src/DarcsWeb/Html.hs:205`, `src/DarcsWeb/Html.hs:229`, `src/DarcsWeb/Html.hs:277`
## Quick wins
1. Stop rescanning every repository from the summary route and fetch only the current repo’s metadata. In `app/Main.hs`, replace the `listRepos`/`findRepo` block with a dedicated single-repo helper, for example:
```hs
-- app/Main.hs
ri <- liftIO $ getSingleRepoInfo repoPath name
patches <- liftIO $ getRepoPatches repoPath
tags <- liftIO $ getRepoTags repoPath
html $ TL.fromStrict $ renderRepoSummary now name ri clone (take 10 patches) tags
```
Then extract the current `checkRepo` logic in `src/DarcsWeb/Darcs.hs` into `getSingleRepoInfo :: FilePath -> Text -> IO RepoInfo`. This removes the cross-repository scan from the hot summary path. `app/Main.hs:238`, `src/DarcsWeb/Darcs.hs:69`
2. Make patch lookup hash-first and diff-second. In `src/DarcsWeb/Darcs.hs`, compare `targetHash` against `makePatchname (info piap)` before calling `extractPatchFull`, e.g.:
```hs
let patchHashOf piap = T.pack (BC.unpack (sha1Show (makePatchname (info piap))))
case find (\piap -> patchHashOf piap == targetHash) (mapRL id patchRL) of
Just piap -> pure (Just (extractPatchFull piap))
Nothing -> pure Nothing
```
That keeps full diff rendering off the non-matching path. `src/DarcsWeb/Darcs.hs:136`, `src/DarcsWeb/Darcs.hs:139`, `src/DarcsWeb/Darcs.hs:263`
3. Split clone caching by object type instead of sending `no-cache` for everything. In `serveClone`, give hashed paths a long immutable policy and keep inventory-like files revalidating:
```hs
if "patches/" `isPrefixOf` subPath || "pristine.hashed/" `isPrefixOf` subPath
then setHeader "Cache-Control" "public, max-age=31536000, immutable"
else setHeader "Cache-Control" "no-cache"
```
This is a large win for repeat clone traffic over slow links. `app/Main.hs:420`, `app/Main.hs:435`
4. Replace the `Text`/`String` HTML escape bridge with a Text-native builder. A minimal direction in `src/DarcsWeb/Html.hs` is:
```hs
esc :: Text -> Text
esc = TL.toStrict . TB.toLazyText . T.foldl' step mempty
```
with `step` appending escaped entities directly. After that, rewrite `highlightDiff` to build one builder instead of concatenating escaped `Text` fragments. `src/DarcsWeb/Html.hs:413`, `src/DarcsWeb/Html.hs:418`, `gen/HtmlPure.hs:37`
5. Replace `String`-based file reads with strict text or bytes reads. For example, in `src/DarcsWeb/Darcs.hs`:
```hs
import qualified Data.Text.IO as TIO
readRepoDescription path = if exists then T.strip <$> TIO.readFile descFile else pure ""
getRepoBlob ... = Just <$> TIO.readFile fullPath
```
If repository files may be non-UTF-8, use `Data.ByteString.readFile` and an explicit decode policy instead of `Prelude.readFile`. `src/DarcsWeb/Darcs.hs:103`, `src/DarcsWeb/Darcs.hs:249`
6. Fix the narrow-screen affordances in the generated HTML: change the summary link text from `...` to `View full shortlog`, add a real `<h1>` to the index page, and add either `caption` or `data-label` hooks to each mobile-collapsed table. Those are small template changes with outsized usability impact. `src/DarcsWeb/Html.hs:56`, `src/DarcsWeb/Html.hs:68`, `src/DarcsWeb/Html.hs:109`, `src/DarcsWeb/Html.hs:154`, `src/DarcsWeb/Html.hs:262`, `src/DarcsWeb/Html.hs:312`
## Deeper restructuring (optional)
- Keep a per-repository metadata snapshot in memory, refreshed on a timer or via mtime checks, so index and summary pages stop reopening repositories and recounting patch histories on demand. `app/Main.hs:230`, `src/DarcsWeb/Darcs.hs:108`
- Move HTML generation to a builder- or template-based layer that emits semantic landmarks by default and can stream larger responses. That would solve the current `div`-heavy structure and remove most of the `Text` concatenation overhead in one pass. `src/DarcsWeb/Html.hs:31`, `src/DarcsWeb/Html.hs:195`, `src/DarcsWeb/Html.hs:413`