Peterbe.com

A blog and website by Peter Bengtsson

Lazy-load Firebase Firestore and Firebase Authentication in Preact

02 September 2020 0 comments   Web development, Web Performance, JavaScript, Preact


I'm working on a Firebase app called That's Groce! based on preact-cli, with TypeScript, and I wanted to see how it appears with or without Firestore and Authenticated lazy-loaded.

In the root, there's an app.tsx that used look like this:

import { FunctionalComponent, h } from "preact";
import { useState, useEffect } from "preact/hooks";

import firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";

import { firebaseConfig } from "./firebaseconfig";

const app = firebase.initializeApp(firebaseConfig);

const App: FunctionalComponent = () => {
  const [auth, setAuth] = useState<firebase.auth.Auth | null>(null);
  const [db, setDB] = useState<firebase.firestore.Firestore | null>(null);

  useEffect(() => {
    const appAuth = app.auth();
    setAuth(appAuth);
    appAuth.onAuthStateChanged(authStateChanged);

    const db = firebase.firestore();
    setDB(db);
  }, []);

...

While this works, it does make a really large bundle when both firebase/firestore and firebase/auth imported in the main bundle. In fact, it looks like this:

▶ ls -lh build/*.esm.js
-rw-r--r--  1 peterbe  staff   510K Sep  1 14:13 build/bundle.0438b.esm.js
-rw-r--r--  1 peterbe  staff   5.0K Sep  1 14:13 build/polyfills.532e0.esm.js

510K is pretty hefty to have to ask the client to download immediately. It's loaded like this (in build/index.html):

<script crossorigin="anonymous" src="/bundle.0438b.esm.js" type="module"></script>
<script nomodule src="/polyfills.694cb.js"></script>
<script nomodule defer="defer" src="/bundle.a4a8b.js"></script>

To lazy-load this

To lazy-load the firebase/firestore and firebase/auth you do this instead:

...

const App: FunctionalComponent = () => {
  const [auth, setAuth] = useState<firebase.auth.Auth | null>(null);
  const [db, setDB] = useState<firebase.firestore.Firestore | null>(null);

  useEffect(() => {
    import("firebase/auth")
      .then(() => {
        const appAuth = app.auth();
        setAuth(appAuth);
        appAuth.onAuthStateChanged(authStateChanged);
      })
      .catch((error) => {
        console.error("Unable to lazy-load firebase/auth:", error);
      });

    import("firebase/firestore")
      .then(() => {
        const db = firebase.firestore();
        setDB(db);
      })
      .catch((error) => {
        console.error("Unable to lazy-load firebase/firestore:", error);
      });
  }, []);

...

Now it looks like this instead:

▶ ls -lh build/*.esm.js
-rw-r--r--  1 peterbe  staff   173K Sep  1 14:24 build/11.chunk.b8684.esm.js
-rw-r--r--  1 peterbe  staff   282K Sep  1 14:24 build/12.chunk.3c1c4.esm.js
-rw-r--r--  1 peterbe  staff    56K Sep  1 14:24 build/bundle.7225c.esm.js
-rw-r--r--  1 peterbe  staff   5.0K Sep  1 14:24 build/polyfills.532e0.esm.js

The total sum of all (relevant) .esm.js files is the same (minus a difference of 430 bytes).

But what does it really look like? The app is already based around that

const [db, setDB] = useState<firebase.firestore.Firestore | null>(null);

so it knows to wait until db is truthy and it displays a <Loading/> component until it's ready.

To test how it loads I used the Chrome Performance devtools with or without the lazy-loading and it's fairly self-explanatory:

Before
Before, no lazy-loading

After
After, with lazy-loading

Clearly, the lazy-loaded has a nicer pattern in that it breaks up the work by the main thread.

Conclusion

It's fairly simple to do and it works. The main bundle becomes lighter and allows the browser to start rendering the Preact component sooner. But it's not entirely obvious if it's that much better. The same amount of JavaScript needs to downloaded and parsed no matter what. It's clearly working as a pattern but it's still pretty hard to judge if it's worth it. Now there's more "swapping".

And the whole page is server-side rendered anyway so in terms of immediate first-render it's probably the same. Hopefully, HTTP2 loading does the right thing but it's not yet entirely clear if the complete benefit is there. I certainly hope that this can improve the "Total Blocking Time" and "Time to Interactive".

The other important thing is that not all imports from firebase/* work in Node because they depend on window. It works for firebase/firestore and firestore/auth but not for firestore/analytics and firestore/performance. Now, I can add those lazy-loaded in the client and have the page rendered in Node for that initial build/index.html.

<datalist> looks great on mobile devices

28 August 2020 0 comments   Web development, Mobile


<datalist> is an underrated HTML API. It's basically a native autocomplete widget that requires 0 JavaScript. What I didn't know is how great it is on mobile devices. Especially the iOS Safari platform which is, to be honest, the only mobile device I have.

What's cool about it is that it's easy to implement, from a developer's point-of-view. But most importantly, it works great for users. The problem usually on mobile devices and autocomplete is that it's hard to find a good spot to display the suggestions. Most autocomplete widgets are a styled form of <div class="results"><ul><li>Suggestion 1</li><li>Suggestion 2</li></ul></div> that usually follows the <input> element. Usually, this simply boils down to screen height real estate. Oftentimes you want to display so much more rich stuff in the autocomplete results but it's hard to fit in a nice list of results between the <input> and the native keyboard display. For example, on this page ...

Lyrics search
Note how the search results get hidden underneath the keyboard.

Demo

The cool thing about <datalist> is that gets embedded in the native mobile keyboard in a sense. But what's extra cool is that the browser will do an OK job of filtering all the options for you, so that you, as a developer, just need to supply all options and the browser will take care of the rest.

I put together a dead-simple app here: https://cnfyl.csb.app/ (source here) which looks like this on iOS:

Sample search

Caveats

The space that the keyboard now populates with suggestions is usually reserved for helping you autocomplete regular words. It still does if you start typing a word that isn't an option. So arguably, the <datalist> options are primarily helping you when it's very likely that the user will type one of the suggestions.

The matching isn't great in my opinion. If you type "ea" it will match "Peaches" but I find it extremely unlikely that that's helping users. (What do you think?) If you've started typing "ea" if there's no match called "Each" or "North East" then it's probably better with no match at all.

Mind you, check out this hack (source here) which takes control of the <option> tags inside the <datalist> by having an event listener on the input. So if the input is "ea" it only matches expressions that are left-word-delimited and discard the rest.

Native filtering
Default/Native filtering

Custom filtering
Custom filtering

Conclusion

It is without a doubt the simplest autocomplete functionality you can buy. I would buy it again.

Perhaps it's not right for every application. Perhaps it's important to be able to include images in your autocomplete suggestions. Either way, the best thing to do is to park this in the back of your mind till next time you're up against the need for some sort of assisted search or choice. Especially if you predict you'll have a lot of users on mobile devices.

Test if two URLs are "equal" in JavaScript

02 July 2020 2 comments   JavaScript


This saved my bacon today and I quite like it so I hope that others might benefit from this little tip.

So you have two "URLs" and you want to know if they are "equal". I write those words, in the last sentence, in quotation marks because they might not be fully formed URLs and what you consider equal might depend on the current business logic.

In my case, I wanted http://www.peterbe.com/path/to?a=b to be considered equal to/path/to#anchor. Because, in this case the both share the exact same pathname (/path/to). So how to do it:

function equalUrls(url1, url2) {
  return (
    new URL(url1, "http://example.com").pathname ===
    new URL(url2, "http://example.com").pathname
  );
}

If you're doing TypeScript, switch the arguments to (url1: string, url2: string).

That "http://example.com" is deliberate and not a placeholder. It's because:

>> new URL("/just/a/path", "http://example.com").pathname
"/just/a/path"
>> new URL("https://www.peterbe.com/a/path", "http://example.com").pathname
"/a/path"

In other words, if you do it like that the first argument to the URL constructor can be with or without a full absolute URL.

Discussion

Be careful with junk. For example new URL(null, 'http://example.com').pathname becomes /null. So you might want to extend the logic to use "falsyness" like this:

  return (
+   url1 && url2 &&
    new URL(url1, "http://example.com").pathname ===
    new URL(url2, "http://example.com").pathname
  );

findMatchesInText - Find line and column of matches in a text, in JavaScript

22 June 2020 0 comments   Node, JavaScript

https://gist.github.com/peterbe/a210437cd282ef0963749520f6b09a1c


I need this function to relate to open-editor which is a Node program that can open your $EDITOR from Node and jump to a specific file, to a specific line, to a specific column.

Here's the code:

function* findMatchesInText(needle, haystack, { inQuotes = false } = {}) {
  const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  let rex;
  if (inQuotes) {
    rex = new RegExp(`['"](${escaped})['"]`, "g");
  } else {
    rex = new RegExp(`(${escaped})`, "g");
  }
  for (const match of haystack.matchAll(rex)) {
    const left = haystack.slice(0, match.index);
    const line = (left.match(/\n/g) || []).length + 1;
    const lastIndexOf = left.lastIndexOf("\n") + 1;
    const column = match.index - lastIndexOf + 1;
    yield { line, column };
  }
}

And you use it like this:

const text = ` bravo
Abra
cadabra

bravo
`;

console.log(Array.from(findMatchesInText("bra", text)));

Which prints:

[
  { line: 1, column: 2 },
  { line: 2, column: 2 },
  { line: 3, column: 5 },
  { line: 5, column: 1 }
]

The inQuotes option is because a lot of times this function is going to be used for finding the href value in unstructured documents that contain HTML <a> tags.

hashin 0.15.0 now copes nicely with under_scores

15 June 2020 0 comments   Python

https://github.com/peterbe/hashin/pull/119


tl;dr hashin 0.15.0 makes package comparison agnostic to underscore or hyphens

See issue #116 for a fuller story. Basically, now it doesn't matter if you write...

hashin python_memcached

...or...

hashin python-memcached

And the same can be said about the contents of your requirements.txt file. Suppose it already had something like this:

python_memcached==1.59 \
    --hash=sha256:4dac64916871bd35502 \
    --hash=sha256:a2e28637be13ee0bf1a8

and you type hashin python-memcached it will do the version comparison on these independent of the underscore or hyphen.

Thank @caphrim007 who implemented this for the benefit of Renovate.

./bin/huey-isnt-running.sh - A bash script to prevent lurking ghosts

10 June 2020 0 comments   Python, Linux, Bash


tl;dr; Here's a useful bash script to avoid starting something when its already running as a ghost process.

Huey is a great little Python library for doing background tasks. It's like Celery but much lighter, faster, and easier to understand.

What cost me almost an hour of hair-tearing debugging today was that I didn't realize that a huey daemon process had gotten stuck in the background with code that wasn't updating as I made changes to the tasks.py file in my project. I just couldn't understand what was going on.

The way I start my project is with honcho which is a Python Foreman clone. The Procfile looks something like this:

elasticsearch: cd /Users/peterbe/dev/PETERBECOM/elasticsearch-7.7.0 && ./bin/elasticsearch -q
web: ./bin/run.sh web
minimalcss: cd minimalcss && PORT=5000 yarn run start
huey: ./manage.py run_huey --flush-locks --huey-verbose
adminui: cd adminui && yarn start
pulse: cd pulse && yarn run dev

And you start that with simply typing:

honcho start

When you Ctrl-C, it kills all those processes but somehow somewhere it doesn't always kill everything. Restarting the computer isn't a fun alternative.

So, to prevent my sanity from draining I wrote this script:

#!/usr/bin/env bash
set -eo pipefail

# This is used to make sure that before you start huey, 
# there isn't already one running the background.
# It has happened that huey gets lingering stuck as a 
# ghost and it's hard to notice it sitting there 
# lurking and being weird.

bad() {
    echo "Huey is already running!"
    exit 1
}

good() {
    echo "Huey is NOT already running"
    exit 0
}

ps aux | rg huey | rg -v 'rg huey' | rg -v 'huey-isnt-running.sh' && bad || good

(If you're wondering what rg is; it's short for ripgrep)

And I change my Procfile accordingly:

-huey: ./manage.py run_huey --flush-locks --huey-verbose
+huey: ./bin/huey-isnt-running.sh && ./manage.py run_huey --flush-locks --huey-verbose

There really isn't much rocket science or brain surgery about this blog post but I hope it inspires someone who's been in similar trenches that a simple bash script can make all the difference.