Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Thu, 15 Aug 2024 01:15:38 +0000 en-US hourly 1 https://wordpress.org/?v=6.6.1 225069128 Fine-Grained Reactivity in Svelte 5 https://frontendmasters.com/blog/fine-grained-reactivity-in-svelte-5/ https://frontendmasters.com/blog/fine-grained-reactivity-in-svelte-5/#respond Wed, 14 Aug 2024 15:05:23 +0000 https://frontendmasters.com/blog/?p=3438 We’ve been looking at the up and coming Svelte 5. We looked at basic features like state, props, and side effects. Then we looked at Snippets, which is a lightweight feature Svelte added for re-using bits of HTML within (for now) a single component.

Article Series

In this post, we’ll take a close look at Svelte’s new fine-grained reactivity.

What is fine-grained reactivity?

The best way to describe fine-grained reactivity is to show what it isn’t, and the best example of non-fine grained reactivity is React. In React, in any component, setting a single piece of state will cause the entire component, and all of the descendent components to re-render (unless they’re created with React.memo). Even if the state you’re setting is rendered in a single, simple <span> tag in the component, and not used anywhere else at all, the entire world from that component on down will be re-rendered.

This may seem absurdly wasteful, but in reality this is a consequence of the design features that made React popular when it was new: the data, values, callbacks, etc., that we pass through our component trees are all plain JavaScript. We pass plain, vanilla JavaScript objects, arrays and functions around our components and everything just works. At the time, this made an incredibly compelling case for React compared to alternatives like Angular 1 and Knockout. But since then, alternatives like Svelte have closed the gap. My first post on Svelte 5 showed just how simple, flexible, and most importantly reliable Svlete’s new state management primitives are. This post will show you the performance wins these primitives buy us.

Premature optimization is still bad

This post will walk through some Svelte templates using trickery to snoop on just how much of a component is being re-rendered when we change state. This is not something you will usually do or care about. As always, write clear, understandable code, then optimize when needed (not before). Svelte 4 is considerably less efficient than Svelte 5, but still much more performant than what React does out of the box. And React is more than fast enough for the overwhelming majority of use cases — so it’s all relative.

Being fast enough doesn’t mean we can’t still look at how much of a better performance baseline Svelte now starts you off at. With a fast-growing ecosystem, and now an incredibly compelling performance story, hopefully this post will encourage you to at least look at Svelte for your next project.

If you’d like to try out the code we’ll be looking at in this post, it’s all in this repo.

Getting started

The code we’ll be looking at is from a SvelteKit scaffolded project. If you’ve never used SvelteKit before that’s totally fine. We’re not really using any SvelteKit features until the very end of this post, and even then it’s just re-hashing what we’ll have already covered.

Throughout this post, we’re going to be inspecting if and when individual bindings in a component are re-evaluated when we change state. There’s various ways to do this, but the simplest, and frankly dumbest, is to force some global, non-reactive, always-changing state into these bindings. What do I mean by that? In the root page that hosts our site, I’m adding this:

<script>
  var __i = 0;
  var getCounter = () => __i++;
</script>

This adds a global getCounter function, as well as the __i variable. getCounter will always return the next value, and if we stick a call to it in our bindings, we’ll be able to snoop on when those bindings are being re-executed by Svelte. If you’re using TypeScript, you can avoid errors when calling this like so:

declare global {
  interface Window {
    getCounter(): number;
  }
}

export {};

This post will look at different pages binding to the same data, declared mostly like this (we’ll note differences as we go).

let tasks = [
  { id: 1, title: "Task A", assigned: "Adam", importance: "Low" },
  { id: 2, title: "Task B", assigned: "Adam", importance: "Medium" },
  { id: 3, title: "Task C", assigned: "Adam", importance: "High" },
  { id: 4, title: "Task D", assigned: "Mike", importance: "Medium" },
  { id: 5, title: "Task E", assigned: "Adam", importance: "High" },
  { id: 6, title: "Task F", assigned: "Adam", importance: "High" },
  { id: 7, title: "Task G", assigned: "Steve", importance: "Low" },
  { id: 8, title: "Task H", assigned: "Adam", importance: "High" },
  { id: 9, title: "Task I", assigned: "Adam", importance: "Low" },
  { id: 10, title: "Task J", assigned: "Mark", importance: "High" },
  { id: 11, title: "Task K", assigned: "Adam", importance: "Medium" },
  { id: 12, title: "Task L", assigned: "Adam", importance: "High" },
];

And we’ll render these tasks with this markup:

<div>
  {#each tasks as t}
    <div>
      <div>
        <span>{t.id + getCounter()}</span>
        <button onclick={() => (t.id += 10)} class="border p-2">Update id</button>
     </div>
     <div>
       <span>{t.title + getCounter()}</span>
       <button onclick={() => (t.title += 'X')} class="border p-2">Update title</button>
     </div>
     <div>
        <span>{t.assigned + getCounter()}</span>
        <button onclick={() => (t.assigned += 'X')} class="border p-2">Update assigned</button>
      </div>
      <div>
        <span>{t.importance + getCounter()}</span>
        <button onclick={() => (t.importance += 'X')} class="border p-2">Update importance</button>
      </div>
    </div>
  {/each}
</div>

The Svelte 4 code we’ll start with uses the on:click syntax for events, but everything else will be the same.

The calls to getCounter inside the bindings will let us see when those bindings are re-executed, since the call to getCounter() will always return a new value.

Let’s get started!

Svelte 4

We’ll render the content we saw above, using Svelte 4.

Plain and simple. But now let’s click any of those buttons, to modify one property, of one of those tasks—it doesn’t matter which.

Notice that the entire component (every binding in the component) re-rendered. As inefficient as this seems, it’s still much better than what React does. It’s not remotely uncommon for a single state update to trigger multiple re-renders of many components.

Let’s see how Svelte 5 improves things.

Svelte 5

For Svelte 5, the code is pretty much the same, except we declare our state like this:

let tasks = $state([
  { id: 1, title: "Task A", assigned: "Adam", importance: "Low" },
  <em>// and so on ...</em>
  { id: 12, title: "Task L", assigned: "Adam", importance: "High" },
]);

We render the page, and see the same as before. If you’re following along in the repo, be sure to refresh the page after navigating, so the page will start over with the global counter.

Now let’s change one piece of state, as before. We’ll update the title for Task C, the third one.

Just like that, only the single piece of state we modified has re-rendered. Svelte was smart enough to leave everything else alone. 99% of the time this won’t make any difference, but if you’re rendering a lot of data on a page, this can be a substantial performance win.

Why did this happen?

This is the default behavior when we pass arrays and objects (and arrays of objects) into the $state rune, like we did with:

let tasks = $state([

Svelte will read everything you pass, set up Proxy objects to track what changes, and update the absolute minimum amount of DOM nodes necessary.

False Coda

We could end the post here. Use the $state primitive to track your reactive data. Svelte will make it deeply reactive, and update whatever it needs to update when you change anything. This will be just fine the vast majority of the time.

But what if you’re writing a web application that has to manage a ton of data? Making everything deeply reactive is not without cost.

Let’s see how we can tell Svelte that only some of our data is reactive. I’ll stress again, laboring over this will almost never be needed. But it’s good to know how it works if it ever comes up.

Rediscovering a long-lost JavaScript feature

Classes in JavaScript have gotten an unfortunately bad reputation. Classes are an outstanding way to declare the structure of a set of objects, which also happen to come with a built-in factory function for creating those objects. Not only that, but TypeScript is deeply integrated with them.

You can declare:

class Person {
  firstName: string;
  lastName: string;

  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

Not only will this provide you a factory function for creating instances of a Person, via new Person('Adam', 'Rackis'), but Person can also be used as a type within TypeScript. You can create variables or function parameters of type Person. It’s one of the few things that exist as a runtime construct and also a TypeScript type.

That said, if you find yourself reaching for extends in order to create deep inheritance hierarchies with classes, please please re-think your decisions.

Anyway, why am I bringing up classes in this post?

Fine-grained reactivity in Svelte 5

If you have a performance-sensitive section of code where you need to mark some properties as non-reactive, you can do this by creating class instances rather than vanilla JavaScript objects. Let’s define a Task class for our tasks. For the properties we want to be reactive, we’ll set default values with the $state() rune. For properties we don’t want to be reactive, we won’t.

class Task {
  id: number = 0;
  title = $state("");
  assigned = $state("");
  importance = $state("");

  constructor(data: Task) {
    Object.assign(this, data);
  }
}

And then we just use that class

let tasks = $state([
  new Task({ id: 1, title: "Task A", assigned: "Adam", importance: "Low" }),
  <em>// and so on</em>
  new Task({ id: 12, title: "Task L", assigned: "Adam", importance: "High" }),
]);

I simplified the class a bit by taking a raw object with all the properties of the class, and assigning those properties with Object.assign. The object literal is typed in the constructor as Task, the same as the class, but that’s fine because of TypeScript’s structural typing.

When we run that, we’ll see the same exact thing as before, except clicking the button to change the id will not re-render anything at all in our Svelte component. To be clear, the id is still changing, but Svelte is not re-rendering. This demonstrates Svelte intelligently not wiring any kind of observability into that particular property.

Side note: if you wanted to encapsulate / protect the id, you could declare id as #id to make it a private property and then expose the value with a getter function.

Going deeper

What if you don’t want these tasks to be reactive at the individual property at all? What if we have a lot of these tasks coming down, and you’re not going to be editing them? So rather than have Svelte set up reactivity for each of the tasks’ properties, you just want the array itself to be reactive.

You basically want to be able to add or remove entries in your array, and have Svelte update the tasks that are rendered. But you don’t want Svelte setting up any kind of reactivity for each property on each task.

This is a common enough use case that other state management systems support this directly, for example, MobX’s observable.shallow. Unfortunately Svelte does not have any such helper, as of yet. That said, it is currently being debated, so keep your eyes open for a $state.shallow() that would do what we’re about to show. But even if it does get added, implementing it ourselves will be a great way to kick the tires of Svelte’s new reactivity system. Let’s see how.

Implementing our own $state.shallow() equivalent

We already saw how passing class instances to an array shut off fine-grained reactivity by default, leaving you to opt-in, as desired, by setting class fields to $state(). But our data are likely coming from a database, as plain (hopefully typed) JavaScript objects, unrelated to any class; more importantly we likely have zero desire to cobble together a class just for this.

So let’s simulate it. Let’s say that a database is providing our Task objects as JS objects. We (of course) have a type for this:

type Task = {
  id: number;
  title: string;
  assigned: string;
  importance: string;
};

We want to put those instances into an array that itself is reactive, but not the individual properties on the tasks. With a tiny bit of cleverness we can make it mostly painless.

class NonReactiveObjectGenerator {
  constructor(data: unknown) {
    Object.assign(this, data);
  }
}

function shallowObservable<T>(data: T[]): T[] {
  let result = $state(data.map(t => new NonReactiveObjectGenerator(t) as T));
  return result;
}

Our NonReactiveObjectGenerator class takes in any object, and then smears all that object’s properties onto itself. And our shallowObservable takes an array of whatever, and maps it onto instances of our NonReactiveObjectGenerator class. This will force each instance to be a class instance, with nothing reactive. The as T is us forcing TypeScript to treat these new instances as whatever type was passed in. This is accurate, but something TypeScript needs help understanding, since it’s not (as of now) able to read and understand our call to Object.assign in the class constructor.

If you closely read my first post on Svelte 5, you might recall that you can’t directly return reactive state from a function, since the state will be read and unwrapped right at the call-site, and won’t be reactive any longer. Normally you’d have to do this:

return {
  get value() {
    return result;
  },
  set value(newData: T[]) {
    result = newData;
  },
};

Why wasn’t that needed here? It’s true, the $state() value will be read at the function’s call site. So with…

let tasks = shallowObservable(getTasks());

…the tasks variable will not be reactive. But the array itself will still be fully reactive. We can still call push, pop, splice and so on. If you can live without needing to re-assign to the variable, this is much simpler. But even if you do need to set the tasks variable to a fresh array of values, you still don’t even need to use variable assignment. Stay tuned.

I changed the initial tasks array to help out in a minute, but the rest is what you’d expect.

const getTasks = () => [
  { id: 1, title: "Task A", assigned: "Adam", importance: "Low" },
  <em>// ...</em>
  { id: 12, title: "Task L", assigned: "Adam", importance: "High" },
];

let tasks = shallowObservable(getTasks());

And with that, rendering should now work, and none of our properties are reactive. Clicking the edit buttons do nothing.

But we can now add a button to push a new task onto our array.

<button
  onclick={() =>
    tasks.value.push(
      new NonReactiveObjectGenerator({
        id: nextId++,
        title: 'New task',
        assigned: 'Adam',
        importance: 'Low'
      }) as Task
    )}
>
  Add new task
</button>

We can even add a delete button to each row.

<button onclick={() => tasks.value.splice(idx, 1)}>
  Delete
</button>

Yes, Svelte’s reactive array is smart enough to understand push and splice.

Editing tasks this way

You might be wondering if we can still actually edit the individual tasks. We assumed the tasks would be read-only, but what if that changes? We’ve been modifying the array and watching Svelte re-render correctly. Can’t we edit an individual task by just cloning the task, updating it, and then re-assigning to that index? The answer is yes, with a tiny caveat.

Overriding an array index (with a new object instance) does work, and makes Svelte update. But we can’t just do this:

tasks[idx] = { ...t, importance: "X" + t };

Since that would make the new object, which is an object literal, deeply reactive. We have to keep using our class. This time, to keep the typings simple, and to keep the code smell that is the NonReactiveObjectGenerator class hidden as much as possible, I wrote up a helper function.

function cloneNonReactive<T>(data: T): T {
  return new NonReactiveObjectGenerator(data) as T;
}

As before, the type assertion is unfortunately needed. This same function could also be used for the add function we saw above, if you prefer.

To prove editing works, we’ll leave the entire template alone, except for the importance field, which we’ll modify like so

  <div>
    <span>{t.importance + getCounter()}</span>
    <button
      onclick={() => {
        const taskClone = cloneNonReactive(t);
        taskClone.importance += 'X';
        tasks[idx] = cloneNonReactive(taskClone);
      }}
    >
      Update importance
    </button>
</div>

Now running shows everything as it’s always been.

If we click the button to change the id, title or assigned value, nothing changes, because we’re still mutating those properties directly (since I didn’t change anything) in order to demonstrate that they’re not reactive. But clicking the button to update the importance field runs the code above, and updates the entire row, showing any other changes we’ve made.

Here I clicked the button to update the title, twice, and then clicked the button to update the importance. The former did nothing, but the latter updated the component to show all changes.

Re-assigning to the tasks array

We saved a bit of convenience by returning our state value directly from our shallowObservable helper, but at the expense of not being able to assign directly to our array. Or did we?

If you know a bit of JavaScript, you might know…

tasks.length = 0;

…is the old school way to clear an array. That works with Svelte; the Proxy object Svelte sets up to make our array observable works with that. Similarly, we can set the array to a fully new array of values (after clearing it like we just saw) like this:

tasks.push(...newArray);

It’s up to you which approach you take, but hopefully Svelte ships a $state.shallow to provide the best of both worlds: the array would be reactive, and so would the binding, since we don’t have to pass it across a function boundary; it would be built directly into $state.

SvelteKit

Let’s wrap up by briefly talking about how data from SvelteKit loaders is treated in terms of reactivity. In short, it’s exactly how you’d expect. First and foremost, if you return a raw array of objects from your loader like this:

export const load = () => {
  return {
    tasks: [
      { id: 1, title: "Task A", assigned: "Adam", importance: "Low" },
      <em>// ...</em>
      { id: 12, title: "Task L", assigned: "Adam", importance: "High" },
    ],
  };
};

Then none of that data will be reactive in your component. This is to be expected. To make data reactive, you need to wrap it in $state(). As of now, you can’t call $state in a loader, only in a universal Svelte file (something that ends in .svelte.ts). Hopefully in the future Svelte will allow us to have loaders named +page.svelte.ts but for now we can throw something like this in a reactive-utils.svelte.ts file.

export const makeReactive = <T>(arg: T[]): T[] => {
  let result = $state(arg);
  return result;
};

Then import it and use it in our loader.

import { makeReactive } from "./reactive-utils.svelte";

export const load = () => {
  return {
    tasks: makeReactive([
      { id: 1, title: "Task A", assigned: "Adam", importance: "Low" },
      <em>// ...</em>
      { id: 12, title: "Task L", assigned: "Adam", importance: "High" },
    ]),
  };
};

Now those objects will support the same fine-grained reactivity we saw before. To customize which properties are reactive, you’d swap in class instances, instead of vanilla object literals, again just like we saw. All the same rules apply.

If you’re wondering why we did this…

export const makeReactive = <T>(arg: T[]): T[] => {
  let result = $state(arg);
  return result;
};

…rather than this…

export const makeReactive = <T>(arg: T[]): T[] => {
  return $state(arg);
};

… the answer is that the latter is simply disallowed. Svelte forces you to only put $state() calls into assignments. It cannot appear as a return value like this. The reason is that while returning $state variables directly across a function boundary works fine for objects and arrays, doing this for primitive values (strings or numbers) would produce a senseless result. The variable could not be re-assigned (same as we saw with the array), but as a primitive, there’d be no other way to edit it. It would just be a non-reactive constant.

Svelte forcing you to take that extra step, and assign $state to a variable before returning, is intended to help prevent you from making that mistake.

Wrapping up

One of the most exciting features of Svelte 5 is the fine-grained reactivity it adds. Svelte was already lightweight, and faster than most, if not all of the alternatives. These additions in version 5 only improve on that. When added to the state management improvements we’ve already covered in prior posts, Svelte 5 really becomes a serious framework option.

Consider it for your next project.

Article Series

]]>
https://frontendmasters.com/blog/fine-grained-reactivity-in-svelte-5/feed/ 0 3438
Exploring the Possibilities of Native JavaScript Decorators https://frontendmasters.com/blog/exploring-the-possibilities-of-native-javascript-decorators/ https://frontendmasters.com/blog/exploring-the-possibilities-of-native-javascript-decorators/#comments Fri, 09 Aug 2024 18:54:47 +0000 https://frontendmasters.com/blog/?p=3381 We’ve known it for a while now, but JavaScript is eventually getting native support for decorators. The proposal is in stage 3 — it’s inevitable! I’m just coming around to explore the feature, and I’m kinda kicking myself for waiting so long, because I’m finding it to be tremendously helpful. Let’s spend some time exploring it.

The Pattern vs The Feature

It’s probably worth clarifying what’s meant by a “decorator.” Most of the time, people are talking about one of two things:

The decorator design pattern

This is the higher-level concept of augmenting or extending a function’s behavior by “decorating” it. Logging is a common example. You might want to know when and with what parameters it’s called, so you wrap it with another function:

function add(a, b) {
  return a + b;
}

function log(func) {
  return function (...args) {
    console.log(
      `method: ${func.name} | `,
      `arguments: ${[...args].join(", ")}`
    );
    return func.call(this, ...args);
  };
}

const addWithLogging = log(add);

addWithLogging(1, 2);
// adding 1 2

There’s no new language-specific feature here. One function simply accepts another as an argument and returns a new, souped-up version. The original function has been decorated.

Decorators as a feature of the language

The decorator feature is a more tangible manifestation of the pattern. It’s possible you’ve seen an older, unofficial version of this before. We’ll keep using the logging example from above, but we’ll first need to refactor a bit because language-level decorators can only be used on class methods, fields, and on classes themselves.

// The "old" decorator API:

function log(target, key, descriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args) {
    console.log(
      `method: ${originalMethod.name} | `,
      `arguments: ${[...args].join(", ")}`
    );

    return originalMethod.apply(this, args);
  };

  return descriptor;
}

class Calculator {
  @log // <-- Decorator applied here.
  add(a, b) {
    return a + b;
  }
}

new Calculator().add(1, 2); // method: add | arguments: 1, 2

Despite being non-standard, there are a number of popular, mature libraries out there that have used this implementation. TypeORMAngular, and NestJS are just a few of the big ones. And I’m glad they have. It’s made building applications with them feel cleaner, more expressive, and easier to maintain.

But because it’s non-standard, it could become problematic. For example, there’s some nuance between how it’s implemented by Babel and TypeScript, which probably caused frustration for engineers moving between applications with different build tooling. Standardization would serve them well.

The Slightly Different Official API

Fortunately, both TypeScript (as of v5) and Babel (via plugin) now support the TC39 version of the API, which is even simpler:

function log(func, context) {
  return function (...args) {
    console.log(
      `method: ${func.name} | `,
      `arguments: ${[...args].join(", ")}`
    );

    func.call(this, ...args);
  };
}

class Calculator {
  @log
  add(a, b) {
    return a + b;
  }
}

new Calculator().add(1, 2); // method: add | arguments: 1, 2

As you can see, there’s much less of a learning curve, and it’s fully interchangeable with many functions that have been used as decorators until now. The only difference is that it’s implemented with new syntax.

Exploring the Use Cases

There’s no shortage of scenarios in which this feature will be handy, but let’s try out a couple that come to mind.

Debouncing & Throttling

Limiting the number of times an action occurs in a given amount of time is an age-old need on the web. Typically, that’s meant reaching for a Lodash utility or rolling an implementation yourself.

Think of a live search box. To prevent user experience issues and network load, you want to debounce those searches, only firing a request when the user has stopped typing for a period of time:

function debounce(func) {
  let timeout = null;

  return function (...args) {
    clearTimeout(timeout);

    timeout = setTimeout(() => {
      func.apply(this, args);
    }, 500);
  };
}

const debouncedSearch = debounce(search);

document.addEventListener('keyup', function(e) {
  // Will only fire after typing has stopped for 500ms.
  debouncedSearch(e.target.value);
});  

But decorators can only be used on a class or its members, so let’s flesh out a better example. You’ve got a ViewController class with a method for handling keyup events:

class ViewController {
  async handleSearch(query) {
    const results = await search(query);

    console.log(`Update UI with:`, results);
  }
}

const controller = new ViewController();

input.addEventListener('keyup', function (e) {
  controller.handleSearch(e.target.value);
});

Using the debounce() method we wrote above, implementation would be clunky. Focusing in on the ViewController class itself:

class ViewController {
  handleSearch = debounce(async function (query) {
    const results = await search(query);

    console.log(`Got results!`, results);
  });
}

You not only need to wrap your entire method, but you also need to switch from defining a class method to an instance property set to the debounced version of that method. It’s a little invasive.

Updating to a Native Decorator

Turning that debounce() function into an official decorator won’t take much. In fact, the way it’s already written fits the API perfectly: it accepts the original function and spits out the augmented version. So, all we need to do is apply it with the @ syntax:

class ViewController {
  @debounce
  async handleSearch(query) {
    const results = await search(query);

    console.log(`Got results!`, results);
  }
}

That’s all it takes — a single line — for the exact same result.

We can also make the debouncing delay configurable by making debounce() accept a delay value and return a decorator itself:

// Accept a delay:
function debounce(delay) {
  let timeout = null;

  // Return the configurable decorator:
  return function (value) {
    return function (...args) {
      clearTimeout(timeout);

      timeout = setTimeout(() => {
        value.call(this, ...args);
      }, delay);
    };
  };
}

Using it just means calling our decorator wrapper as a function and passing the value:

class ViewController {
  @debounce(500)
  async handleSearch(query) {
    const results = await search(query);

    console.log(`Got results!`, results);
  }
}

That’s a lot of value for minimal code wrangling, especially support being provided by TypeScript and Babel — tools already well-integrated in our build processes.

Memoization

Whenever I think of great memoization that’s syntactically beautiful, Ruby first comes to mind. I’ve written about how elegant it is in the past; the ||= operator is all you really need:

def results
  @results ||= calculate_results
end

But with decorators, JavaScript’s making solid strides. Here’s a simple implementation that caches the result of a method, and uses that value for any future invocations:

function memoize(func) {
  let cachedValue;

  return function (...args) {
    // If it's been run before, return from cache.
    if (cachedValue) {
      return cachedValue;
    }

    cachedValue = func.call(this, ...args);

    return cachedValue;
  };
}

The nice thing about this is that each invocation of a decorator declares its own scope, meaning you can reuse it without risk of the cachedValue being overwritten with an unexpected value.

class Student {
  @memoize
  calculateGPA() {
    // Expensive computation...
    return 3.9;
  }

  @memoize
  calculateACT() {
    // Expensive computation...
    return 34;
  }
}

const bart = new Student();

bart.calculateGPA();
console.log(bart.calculateGPA()); // from cache: 3.9

bart.calculateACT();
console.log(bart.calculateACT()); // from cache: 34

Going further, we could also memoize based on the parameters passed to a method:

function memoize(func) {
  // A place for each distinct set of parameters.
  let cache = new Map();

  return function (...args) {
    const key = JSON.stringify(args);

    // This set of parameters has a cached value.
    if (cache.has(key)) {
      return cache.get(key);
    }

    const value = func.call(this, ...args);

    cache.set(key, value);

    return value;
  };
}

Now, regardless of parameter usage, memoization can become even more flexible:

class Student {
  @memoize
  calculateRank(otherGPAs) {
    const sorted = [...otherGPAs].sort().reverse();

    for (let i = 0; i <= sorted.length; i++) {
      if (this.calculateGPA() > sorted[i]) {
        return i + 1;
      }
    }

    return 1;
  }

  @memoize
  calculateGPA() {
    // Expensive computation...
    return 3.4;
  }
}

const bart = new Student();

bart.calculateRank([3.5, 3.7, 3.1]); // fresh
bart.calculateRank([3.5, 3.7, 3.1]); // cached
bart.calculateRank([3.5]); // fresh

That’s cool, but it’s also worth noting that you could run into issues if you’re dealing with parameters that can’t be serialized (undefined, objects with circular references, etc.). So, use it with some caution.

Memoizing Getters

Since decorators can be used on more than just methods, a slight adjustment means we can memoize getters too. We just need to use context.name (the name of the getter) as the cache key:

function memoize(func, context) {
  let cache = new Map();

  return function () {
    if (cache.has(context.name)) {
      return cache.get(context.name);
    }

    const value = func.call(this);

    cache.set(context.name, value);

    return value;
  };
}

Implementation would look the same:

class Student {
  @memoize
  get gpa() {
    // Expensive computation...
    return 4.0;
  }
}

const milton = new Student();

milton.gpa // fresh
milton.gpa // from the cache

That context object contains some useful bits of information, by the way. One of those is the “kind” of field being decorated. That means we could even take this a step further by memoizing the getters and methods with the same decorator:

function memoize(func, context) {
  const cache = new Map();

  return function (...args) {
    const { kind, name } = context;

    // Use different cache key based on "kind."
    const cacheKey = kind === 'getter' ? name : JSON.stringify(args);

    if (cache.has(cacheKey)) {
      return cache.get(cacheKey);
    }

    const value = func.call(this, ...args);

    cache.set(cacheKey, value);

    return value;
  };
}

You could take this much further, but we’ll draw the line there for now, and instead shift to something a little more complex.

Dependency Injection

If you’ve worked with a framework like Laravel or Spring Boot, you’re familiar with dependency injection and the “inversion of control (IoC) container” for an application. It’s a useful feature, enabling you to write components more loosely coupled and easily testable. With native decorators, it’s possible to bring that core concept to vanilla JavaScript as well. No framework needed.

Let’s say we’re building an application needing to send messages to various third-parties. Triggering an email, sending an analytics event, firing a push notification, etc. Each of these are abstracted into their own service classes:

class EmailService {
  constructor() {
    this.emailKey = process.env.EMAIL_KEY;
  }
}

class AnalyticsService {
  constructor(analyticsKey) {
    this.analyticsKey = analyticsKey;
  }
}

class PushNotificationService {
  constructor() {
    this.pushNotificationKey = process.env.PUSH_NOTIFICATION_KEY;
  }
}

Without decorators, it’s not difficult to instantiate those yourself. It might look something like this:

class MyApp {
  constructor(
    emailService = new EmailService(),
    analyticsService = new AnalyticsService(),
    pushNotificationService = new PushNotificationService()
  ) {
    this.emailService = emailService;
    this.analyticsService = analyticsService;
    this.pushNotificationService = pushNotificationService;

    // Do stuff...
  }
}

const app = new MyApp();

But now you’ve cluttered your constructor with parameters that’ll never otherwise be used during runtime, and you’re taking on full responsibility for instantiating those classes. There are workable solutions out there (like relying on separate modules to create singletons), but it’s not ergonomically great. And as complexity grows, this approach will become more cumbersome, especially as you attempt to maintain testability and stick to good inversion of control.

Dependency Injection with Decorators

Now, let’s create a basic dependency injection mechanism with decorators. It’ll be in charge of registering dependencies, instantiating them when necessary, and storing references to them in a centralized container.

In a separate file (container.js), we’ll build a simple decorator used to register any classes we want to make available to the container.

const registry = new Map();

export function register(args = []) {
  return function (clazz) {
    registry.set(clazz, args);
  };
}

There’s not much to it. We’re accepting the class itself and optional constructor arguments needed to spin it up. Next up, we’ll create a container to hold the instances we create, as well as an inject() decorator.

const container = new Map();

export function inject(clazz) {
  return function (_value, context) {
    context.addInitializer(function () {
      let instance = container.get(clazz);

      if (!instance) {
        instance = Reflect.construct(clazz, registry.get(clazz));
        container.set(clazz, instance);
      }

      this[context.name] = instance;
    });
  };
}

You’ll notice we’re using something else from the decorator specification. The addInitializer() method will fire a callback only after the decorated property has been defined. That means we’ll be able to lazily instantiate our injected dependencies, rather than booting up every registered class all at once. It’s a slight performance benefit. If a class uses the EmailService for example, but it’s never actually instantiated, we won’t unnecessarily boot up an instance of EmailService either.

That said, here’s what’s going on when the decorator is invoked:

  • We check for any active instance of the class in our container.
  • If we don’t have one, we create one using the arguments stored in the registry, and store it in the container.
  • That instance is assigned to the name of the field we’ve decorated.

Our application can now handle dependencies a little more elegantly.

import { register, inject } from "./container";

@register()
class EmailService {
  constructor() {
    this.emailKey = process.env.EMAIL_KEY;
  }
}
@register()
class AnalyticsService {
  constructor(analyticsKey) {
    this.analyticsKey = analyticsKey;
  }
}
@register()
class PushNotificationService {
  constructor() {
    this.pushNotificationKey = process.env.PUSH_NOTIFICATION_KEY;
  }
}

class MyApp {
  @inject(EmailService)
  emailService;

  @inject(AnalyticsService)
  analyticsService;

  @inject(PushNotificationService)
  pushNotificationService;

  constructor() {
    // Do stuff.
  }
}

const app = new MyApp();

And as an added benefit, it’s straightforward to substitute those classes for mock versions of them as well. Rather than overriding class properties, we can less invasively inject our own mock classes into the container before the class we’re testing is instantiated:

import { vi, it } from 'vitest';
import { container } from './container';
import { MyApp, EmailService } from './main';

it('does something', () => {
  const mockInstance = vi.fn();
  container.set(EmailService, mockInstance);

  const instance = new MyApp();
  
  // Test stuff.
});

That makes for less responsibility on us, tidy inversion of control, and straightforward testability. All made easy by a native feature.

Just Scratching the Surface

If you read through the proposal, you’ll see that the decorator specification is far deeper than what’s been explored here, and will certainly open up some novel use cases in the future, especially once more runtimes support it. But you don’t need to master the depths of the feature in order to benefit. At its foundation, the decorator feature is still firmly seated on the decorator pattern. If you keep that in mind, you’ll be in a strong position to greatly benefit from it in your own code.

]]>
https://frontendmasters.com/blog/exploring-the-possibilities-of-native-javascript-decorators/feed/ 1 3381
Reading from the Clipboard in JavaScript https://frontendmasters.com/blog/reading-from-the-clipboard-in-javascript/ https://frontendmasters.com/blog/reading-from-the-clipboard-in-javascript/#respond Wed, 31 Jul 2024 14:22:50 +0000 https://frontendmasters.com/blog/?p=3136 Browsers have excellent support for reading and writing the user’s clipboard, and this opens up possibilities for better, and more native like experiences on the web. On websites that use these APIs for helpful features, it feels natural to the user. On sites where it isn’t supported, it almost feels like a bug. In this series of articles, I’m going to demonstrate how to work with the clipboard.

Article Series
  1. Reading from the Clipboard (You are here!)
  2. Writing to the Clipboard (Coming soon!)
  3. Handling Pasted Content from the Clipboard (Coming soon!)

Before We Begin…

Clipboard functionality on the web requires a “secure context”. So if you’re running an http site (as opposed to an https site), these features will not work. I’d highly encourage you to get your site on https. That being said, these features, and others like them that require secure contexts, will still work on http://localhost. There’s no need to set up a temporary certificate when doing local testing.

Also note that some browsers will manipulate the content of the clipboard when read. This is done for security reasons and you can disable it. I’ll demonstrate this later in the article.

The Clipboard API

The Clipboard API is the top-level object (navigator.clipboard) containing the methods to work with the clipboard. According to MDN, support is pretty much across the board:

In fact, the only outlier relates to being able to disable the security aspects mentioned above in Firefox and Safari. Outside of that, support is great.

Reading the Clipboard

Reading from the clipboard is handled by two methods:

  • read
  • readText

Which you use will depend on your particular use case. In theory, read would be the most flexible, handling any content, but if you know for a fact you only need to support text input from the clipboard, you should probably use the more specific readText format. I’m a fan of code that helps reduce my chance of mistakes so that’s what I’d recommend. Let’s start with an example of that.

First, I’ll use a bit of HTML:

<button id="readCB">Read Clipboard</button>
<div id="log"></div>

The button will be used to kick off the code to read from the clipboard and I’ll use the <div> to show the results. Now the JavaScript:

let $log = document.querySelector('#log');

document.querySelector('#readCB').addEventListener('click', async () => {
  let contents = await navigator.clipboard.readText();
  console.log(contents);
  $log.innerText += `From clipboard: ${contents}`;
});

When the button is clicked, I try to read text from the clip. Note I said try. The first time the button is clicked, the browser prompts for permission:

Chrome
Arc
If you need to revoke or change permissions…

There should be some UI in the browser bar area to do this. On Chrome it’s the little control icon which opens this:

Chrome controls panel for a website showing secure connection, and also the Clipboard access which can be turned off or reset.

The process of reading from the clipboard is async, so I’m using await to make that a bit simpler. Then, I log the contents to the console and write it out to the DOM.

Here’s a live demo, but please note that it may not work for you, because of the permissions prompts mentioned above. You can view this demo here and that will allow you to approve the permissions to make it work. It’s tricky with <iframe> demos — both domains need the permissions approved, but won’t prompt for them, and even then it doesn’t always want to work.

How well does it work?

If you have nothing on the clipboard, the code runs just fine, but the contents are an empty string. It was actually a bit difficult to figure out how to empty my clipboard. I initially tested by putting my cursor on an empty line in my editor and hitting CTRL+C, but that felt like a hack. This random article showed a command line prompt I could run on Windows that seemed to be more “official”. I used the suggested command and got the exact same result. No error, just an empty string.

Copying text works as expected, but note that if you copy HTML, and insert it into the DOM, it will be rendered as HTML. In my case, I’m using innerText so that’s not an issue.

Next, I tested “rendered” text, and by that I mean random text from a web page, ensuring I got styled text as well. As expected, I got just the text of the selection from the HTML. So for example:

Which ended up as:

CSS
MIXINS
STYLE QUERIES
Style Queries are Almost Like Mixins (But Mixins Would Be Better)
By CHRIS COYIER on July 12, 2024
Having a named block of styles to apply in CSS can be useful, and newfangled Style Queries are pretty close to that. We look at one use case here, how Sass did mixins better, and hope for a native solution.

I tried copying from Word and PDF and got the same type of result.

Cool — now let’s kick it up a notch!

Reading Multimedia Content from the Clipboard

As explained earlier, the read method can support any content, not just text. Switching to it is as simple as:

let contents = await navigator.clipboard.read();

This method returns an array of ClipboardItems representing the fact that a user may have selected multiple different items. A ClipboardItem consists of:

  • types: An array of MIME types associated with the item
  • presentationStyle: This was one of the rare times when MDN failed to be helpful and I had to actually go to the spec. This property represents whether the pasted content should be considered as appended content or an attachment. You can imagine how an email client works where if you paste text, it just adds to the current email, but binary data is usually handled as an attachment to the mail instead. You don’t really need to worry about it as it’s only supported in Firefox. That being said, you can make up your own mind depending on the mimetype being used.

To get the actual contents you need to use the getType method, which feels oddly named. It takes a mimetype as an argument and returns a blob.

Now things get a bit more complex. Your application has to figure out what makes sense to do based on the data in the clipboard. Let’s consider a more advanced version of the previous demo:

document.querySelector('#readCB').addEventListener('click', async () => {
  let contents = await navigator.clipboard.read();

  for (let item of contents) {

    console.log('Types for this item: ', item.types);

    if (item.types.includes('text/html')) {
      let blob = await item.getType('text/html');
      let html = await blob.text();
      console.log(html);
      $log.innerHTML += html.replaceAll('<','&lt;').replaceAll('>','&gt;');
      $log.innerHTML += '<hr>';
    }

    if (item.types.includes('text/plain')) {
      let blob = await item.getType('text/plain');
      let text = await blob.text();
      console.log(text);
      $log.innerHTML += text.replaceAll('<','&lt;').replaceAll('>','&gt;');
      $log.innerHTML += '<hr>';
    }

    if (item.types.includes('image/png')) {
      // modified from MDN sample
      const pngImage = new Image(); 
      pngImage.alt = "PNG image from clipboard";
      const blob = await item.getType("image/png");
      pngImage.src = URL.createObjectURL(blob);
      $log.appendChild(pngImage);
    }

  }
});

Remember: each item in the array has itself an array of mimetypes. This is very important because many places where you copy code (web pages, your IDE, etc.) may return both text/plain and text/html for an item. That’s… good, as it gives you options in terms of what you want to do. If you want to try to keep some of the original formatting, get the HTML. If you only care about the content, get just the plain text.

Here’s another live demo, this one handling multiple content types.

Copying from a PDF

Surprisingly, copying from a PDF will only return text/plain, even if you select an entire document and it has images. This makes some sense though. Check out the screenshot from Acrobat below, where I had run “Select All”:

As you can see above, the textual elements are highlighted, not the image. You can copy an image from a PDF, but you can have to click on it specifically.

Security Concerns and Sanitization

The MDN docs mention this in regards to reading from the clipboard:

Certain browsers may sanitize the clipboard data when it is read, to prevent malicious content from being pasted into the document. For example, Chrome (and other Chromium-based browsers) sanitizes HTML data by stripping <script> tags and other potentially dangerous content. Use the unsanitized array to specify a list of MIME types that should not be sanitized.

The way around this is to pass a formats object to the read method that specifies an array of mimetypes the browser should not sanitize. Given a clipboard with potentially dangerous content, that means you would expect a different result from

let contents = await navigator.clipboard.read();

versus:

let contents = await navigator.clipboard.read({ unsanitized: ['text/html'] });

This was fairly difficult to test if I selected code that includes the <script> tag, it only reported text/plain as a mimetype. The only way I was able to verify this was in the MDN example where they kinda “force” the issue by writing to the clipboard as HTML. Honestly, I can’t see a reason to use this particular feature unless more ‘sanitized’ things turn up. I’d recommend just letting the browser clear it out if it needs to. But to be clear, if you copy code that includes the <script> tag, it will come in as text/plain and be read just fine.

Caveats

Though a user may have a file from the filesystem selected, if you try to read it from the clipboard, you get an empty array of results. I found this surprising as you can paste files into the browser and support reading them. We’ll get into that in the article on pasting.

Examples Use Cases

How about some examples to give you ideas of how you would use this in the real world?

Addresses

Sites making use of client-side maps (like Google Maps or Leaflet) could read from the clipboard and attempt to parse the contents as an address, and if found, focus the map. This will probably require the use of a Geocoding API to translate a freeform address into longitude and latitude points.

Google Maps on iOS (while not technically a web app) behaves this way. If you’ve given permission, if you happen to have an address on your clipboard you’ll see it as a one-click option right up top.

QR Codes

How about a TamperMonkey script that lets you take any block of text in your clipboard and turn it into a QR code? My buddy Todd Sharp literally built this the day after I wrote the first draft of this article. His work could be used in a regular web page as well to product QR codes from the clipboard.

URLs

If you knew the user had a URL on their clipboard, your app could offer to do something with it. Perhaps automatically add useful URL params, shorten it, or otherwise.


Can you think of any other use cases for reading from the clipboard? Admittedly, writing to the clipboard is generally a more common and useful ability, and we’ll get to that next.

]]>
https://frontendmasters.com/blog/reading-from-the-clipboard-in-javascript/feed/ 0 3136
Patterns for Memory Efficient DOM Manipulation with Modern Vanilla JavaScript https://frontendmasters.com/blog/patterns-for-memory-efficient-dom-manipulation/ https://frontendmasters.com/blog/patterns-for-memory-efficient-dom-manipulation/#comments Mon, 29 Jul 2024 12:47:16 +0000 https://frontendmasters.com/blog/?p=1551 Let’s continue the modern vanilla JavaScript series!

Memory Efficient DOM Manipulation

Article Series

I’ll discuss best practices to avoid excess memory usage when managing updating the DOM to make your apps blazingly fast™️.

DOM: Document Object Model – A Brief Overview

When you render HTML, the live view of those rendered elements in the browser is called the DOM. This is what you’ll see in your developer tools “Elements” inspector:

Elements panel in chrome dev tools

It’s essentially a tree, with each element inside of it being a leaf. There is an entire set of APIs specifically dealing with modifying this tree of elements.

Here’s a quick list of common DOM APIs:

  • querySelector()
  • querySelectorAll()
  • createElement()
  • getAttribute()
  • setAttribute()
  • addEventListener()
  • appendChild()

These are attached to the document, so you use them like const el = document.querySelector("#el");. They are also available on all other elements, so if you have an element reference you can use these methods and their abilities are scoped to that element.

const nav = document.querySelector("#site-nav");
const navLinks = nav.querySelectorAll("a");

These methods will be available in the browser to modify the DOM, but they won’t be available in server JavaScript (like Node.js) unless you use a DOM emulator like js-dom.

As an industry, we’ve offloaded most of this direct rendering to frameworks. All JavaScript frameworks (React, Angular, Vue, Svelte, etc) use these APIs under the hood. While I recognize that the productivity benefits of frameworks often outweigh the potential performance gains of manual DOM manipulation, I want to demystify what goes on under the hood in this article.

Why manipulate the DOM yourself in the first place?

The main reason is performance. Frameworks can add unnecessary data structures and re-renders leading to the dreaded stuttering / freezing behavior seen in many modern web apps. This is due to the Garbage Collector being put on overdrive having to handle all that code.

The downside is it is more code to handle DOM manipulation yourself. It can get complicated, which is why it a better developer experience to use frameworks and abstractions around the DOM rather than manipulating the DOM manually. Regardless, there are cases where you may need the extra performance. That is what this guide is for.

VS Code is Built on Manual DOM Manipulation

Visual Studio Code is one of those such cases. VS Code is written in vanilla JavaScript “to be as close to the DOM as possible.” Projects as large as VS Code need to have tight control over performance. Since much of the power is in the plugins ecosystem, the core needs to be as core and lightweight as possible and is responsible for its wide adoption.

Microsoft Edge also recently moved off of React for the same reason.

If you find yourself in this case where you need the performance of direct DOM manipulation – lower level programming than using a framework – hopefully this article will help!

Tips for More Efficient DOM Manipulation

Prefer hiding/showing over creating new elements

Keeping your DOM unchanged by hiding and showing elements instead of destroying and creating them with JavaScript is always going to be the more performant option.

Server render your element and hide/show it with a class (and appropriate CSS ruleset) like el.classList.add('show') or el.style.display = 'block' instead of creating and inserting the element dynamically with JavaScript. The mostly static DOM is much more performant due to the lack of garbage collection calls and complex client logic.

Don’t create DOM nodes on the client dynamically if you can avoid it.

But do remember assistive technology. If you want an element both visually hidden and hidden to assistive technology, display: none; should do it. But if you want to hide an element and keep it there for assistive technology, look at other methods for hiding content.

Prefer textContent over innerText for reading the content of an element

The innerText method is cool because it is aware of the current styles of an element. It knows if an element is hidden or not, and only gets text if something is actually displaying. The issue with it is that this process of checking styles forces reflow, and is slower.

Reading content with element.textContent is much faster than element.innerText, so prefer textContent for reading content of an element where possible.

Use insertAdjacentHTML over innerHTML

The insertAdjacentHTML method is much faster than innerHTML because it doesn’t have to destroy the DOM first before inserting. The method is flexible in where it places the new HTML, for example:

el.insertAdjacentHTML("afterbegin", html);
el.insertAdjacentHTML("beforeend", html);

The Fastest Approach is to use insertAdjacentElement or appendChild

Approach #1: Use the template tag to create HTML templates and appendChild to insert new HTML

These are the fastest methods are to append fully formed DOM elements. An established pattern for this is to create an HTML template with the <template> tag to create the elements, then insert them into the DOM with insertAdjacentElement or appendChild methods.

<template id="card_template">
  <article class="card">
    <h3></h3>
    <div class="card__body">
      <div class='card__body__image'></div>
      <section class='card__body__content'>
      </section>
    </div>
  </article>
</template>
function createCardElement(title, body) {
  const template = document.getElementById('card_template');
  const element = template.content.cloneNode(true).firstElementChild;
  const [cardTitle] = element.getElementsByTagName("h3");
  const [cardBody] = element.getElementsByTagName("section");
  [cardTitle.textContent, cardBody.textContent] = [title, body];
  return element;
}

container.appendChild(createCardElement(
  "Frontend System Design: Fundamentals",
  "This is a random content"
))

You can see this in action in the new, Front-End System Design course, where Evgenni builds an infinite scrolling social news feed from scratch!

Approach #2: Use createDocumentFragment with appendChild to Batch Inserts

DocumentFragment is a lightweight, “empty” document object that can hold DOM nodes. It’s not part of the active DOM tree, making it ideal for preparing multiple elements for insertion.

const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  fragment.appendChild(li);
}
document.getElementById('myList').appendChild(fragment);

This approach minimizes reflows and repaints by inserting all elements at once, rather than individually.

Manage References When Nodes are Removed

When you remove a DOM node, you don’t want references sitting around that prevent the garbage collector from cleaning up associated data. We can use WeakMap and WeakRef to avoid leaky references.

Associate data to DOM nodes with WeakMap

You can associate data to DOM nodes using WeakMap. That way if the DOM node is removed later, the reference to the data will be gone for good.

let DOMdata = { 'logo': 'Frontend Masters' };
let DOMmap = new WeakMap();
let el = document.querySelector(".FmLogo");
DOMmap.set(el, DOMdata);
console.log(DOMmap.get(el)); // { 'logo': 'Frontend Masters' }
el.remove(); // DOMdata is able to be garbage collected

Using weak maps ensures references to data doesn’t stick around if a DOM element is removed.

Clean up after Garbage Collection using WeakRef

In the following example, we are creating a WeakRef to a DOM node:

class Counter {
  constructor(element) {
    // Remember a weak reference to the DOM element
    this.ref = new WeakRef(element);
    this.start();
  }

  start() {
    if (this.timer) {
      return;
    }

    this.count = 0;

    const tick = () => {
      // get the element from the weak reference, if it still exists
      const element = this.ref.deref();
      if (element) {
        console.log("Element is still in memory, updating count.")
        element.textContent = `Counter: ${++this.count}`;
      } else {
        // The element doesn't exist anymore
        console.log("Garabage Collector ran and element is GONE – clean up interval");
        this.stop();
        this.ref = null;
      }
    };

    tick();
    this.timer = setInterval(tick, 1000);
  }

  stop() {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = 0;
    }
  }
}

const counter = new Counter(document.getElementById("counter"));
setTimeout(() => {
  document.getElementById("counter").remove();
}, 5000);

After removing the node, you can watch your console to see when the actual garbage collection happens, or you can force it to happen yourself using the Performance tab in your developer tools:

Then you can be sure all references are gone and timers are cleaned up.

Note: Try not to overuse WeakRef—this magic does come at a cost. It’s better for performance if you can explicitly manage references.

Cleaning up Event Listeners

Manually remove events with removeEventListener

function handleClick() {
  console.log("Button was clicked!");
  el.removeEventListener("click", handleClick);
}

// Add an event listener to the button
const el = document.querySelector("#button");
el.addEventListener("click", handleClick);

Use the once param for one and done events

This same behavior as above could be achieved with the “once” param:

el.addEventListener('click', handleClick, {
  once: true
});

Adding a third parameter to addEventListener with a boolean value indicating that the listener should be invoked at most once after being added. The listener is automatically removed when invoked.

Use event delegation to bind fewer events

If you are building up and replacing nodes frequently in a highly dynamic component, it’s more expensive to have to setup all their respective event listeners as you’re building the nodes.

Instead, you can bind an event closer to the root level. Since events bubble up the DOM, you can check the event.target (original target of the event) to catch and respond to the event.

Using matches(selector) only matches the current element, so it needs to be the leaf node.

const rootEl = document.querySelector("#root");
// Listen for clicks on the entire window
rootEl.addEventListener('click', function (event) {
  // if the element is clicked has class "target-element"
  if (event.target.matches('.target-element')) doSomething();
});

More than likely, you’ll have elements like <div class="target-element"><p>...</p></div> in this case you’d need to use the .closest(element) method.

const rootEl = document.querySelector("#root");
// Listen for clicks on the entire window
rootEl.addEventListener('click', function (event) {
  // if the element is clicked has a parent with "target-element"
  if (event.target.closest('.target-element')) doSomething();
});

This method allows you to not worry about attaching and removing listeners after dynamically injecting elements.

Use AbortController to unbind groups of events

const button = document.getElementById('button');
const controller = new AbortController();
const { signal } = controller;

button.addEventListener(
  'click', 
  () => console.log('clicked!'), 
  { signal }
);

// Remove the listener!
controller.abort();

You can use the AbortController to remove sets of events.

let controller = new AbortController();
const { signal } = controller;

button.addEventListener('click', () => console.log('clicked!'), { signal });
window.addEventListener('resize', () => console.log('resized!'), { signal });
document.addEventListener('keyup', () => console.log('pressed!'), { signal });

// Remove all listeners at once:
controller.abort();

Shout out to Alex MacArthur for the AbortController code example on this one.

Profiling & Debugging

Measure your DOM to make sure it is not too large.

Here’s a brief guide on using Chrome DevTools for memory profiling:

  1. Open Chrome DevTools
  2. Go to the “Memory” tab
  3. Choose “Heap snapshot” and click “Take snapshot”
  4. Perform your DOM operations
  5. Take another snapshot
  6. Compare snapshots to identify memory growth

Key things to look for:

  • Unexpectedly retained DOM elements
  • Large arrays or objects that aren’t being cleaned up
  • Increasing memory usage over time (potential memory leak)

You can also use the “Performance” tab to record memory usage over time:

  1. Go to the “Performance” tab
  2. Check “Memory” in the options
  3. Click “Record”
  4. Perform your DOM operations
  5. Stop recording and analyze the memory graph

This will help you visualize memory allocation and identify potential leaks or unnecessary allocations during DOM manipulation.

JavaScript Execution Time Analysis

In addition to memory profiling, the Performance tab in Chrome DevTools is invaluable for analyzing JavaScript execution time, which is crucial when optimizing DOM manipulation code.

Here’s how to use it:

  1. Open Chrome DevTools and go to the “Performance” tab
  2. Click the record button
  3. Perform the DOM operations you want to analyze
  4. Stop the recording
Excerpt from ThePrimeagen's 
Inspecting & Debugging Performance lesson

The resulting timeline will show you:

  • JavaScript execution (yellow)
  • Rendering activities (purple)
  • Painting (green)

Look for:

  • Long yellow bars, indicating time-consuming JavaScript operations
  • Frequent short yellow bars, which might indicate excessive DOM manipulation

To dive deeper:

  • Click on a yellow bar to see the specific function call and its execution time
  • Look at the “Bottom-Up” and “Call Tree” tabs to identify which functions are taking the most time

This analysis can help you pinpoint exactly where your DOM manipulation code might be causing performance issues, allowing for targeted optimizations.

Performance Debugging Resources

Articles by the Chrome dev team:

Courses that cover the memory and performance analysis and Chrome Dev Tools further:

Remember, efficient DOM manipulation isn’t just about using the right methods—it’s also about understanding when and how often you’re interacting with the DOM. Excessive manipulation, even with efficient methods, can still lead to performance issues.

Key Takeaways for DOM Optimization

Efficient DOM manipulation knowledge is important when creating performance-sensitive web apps. While modern frameworks offer convenience and abstraction, understanding and applying these low-level techniques can significantly boost your app’s performance, especially in demanding scenarios.

Here’s a recap:

  1. Prefer modifying existing elements over creating new ones when possible.
  2. Use efficient methods like textContent, insertAdjacentHTML, and appendChild.
  3. Manage references carefully, leveraging WeakMap and WeakRef to avoid memory leaks.
  4. Clean up event listeners properly to prevent unnecessary overhead.
  5. Consider techniques like event delegation for more efficient event handling.
  6. Use tools like AbortController for easier management of multiple event listeners.
  7. Employ DocumentFragments for batch insertions and understand concepts like the virtual DOM for broader optimization strategies.

Remember, the goal isn’t always to forgo frameworks and manually manipulate the DOM for every project. Rather, it’s to understand these principles so you can make informed decisions about when to use frameworks and when to optimize at a lower level. Tools like memory profiling and performance benchmarking can guide these decisions.

Article Series

]]>
https://frontendmasters.com/blog/patterns-for-memory-efficient-dom-manipulation/feed/ 5 1551
Introducing Svelte 5 https://frontendmasters.com/blog/introducing-svelte-5/ https://frontendmasters.com/blog/introducing-svelte-5/#comments Fri, 19 Jul 2024 18:40:23 +0000 https://frontendmasters.com/blog/?p=3067 Svelte has always been a delightful, simple, and fun framework to use. It’s a framework that’s always prioritized developer experience (DX), while producing a light and fast result with minimal JavaScript. It achieves this nice DX by giving users dirt simple idioms and a required compiler that makes everything work. Unfortunately, it used to be fairly easy to break Svelte’s reactivity. It doesn’t matter how fast a website is if it’s broken.

These reliability problems with reactivity are gone in Svelte 5.

In this post, we’ll get into the exciting Svelte 5 release (in Beta at the time of this writing). Svelte is the latest framework to add signals to power their reactivity. Svelte is now every bit as capable of handling robust web applications, with complex state, as alternatives like React and Solid. Best of all, it achieved this with only minimal hits to DX. It’s every bit as fun and easy to use as it was, but it’s now truly reliable, while still producing faster and lighter sites.

Article Series

Let’s jump in!

The Plan

Let’s go through various pieces of Svelte, look at the “old” way, and then see how Svelte 5 changes things for the better. We’ll cover:

  1. State
  2. Props
  3. Effects

If you find this helpful, let me know, as I’d love to cover snippets and Svelte’s exciting new fine-grained reactivity.

As of this writing, Svelte 5 is late in the Beta phase. The API should be stable, although it’s certainly possible some new things might get added.

The docs are also still in beta, so here’s a preview URL for them. Svelte 5 might be released when you read this, at which point these docs will be on the main Svelte page. If you’d like to see the code samples below in action, you can find them in this repo.

State

Effectively managing state is probably the most crucial task for any web framework, so let’s start there.

State used to be declared with regular, plain old variable declarations, using let.

let value = 0;

Derived state was declared with a quirky, but technically valid JavaScript syntax of $:. For example:

let value = 0;
$: doubleValue = value * 2;

Svelte’s compiler would (in theory) track changes to value, and update doubleValue accordingly. I say in theory since, depending on how creatively you used value, some of the re-assignments might not make it to all of the derived state that used it.

You could also put entire code blocks after $: and run arbitrary code. Svelte would look at what you were referencing inside the code block, and re-run it when those things changed.

$: {
  console.log("Value is ", value);
}

Stores

Those variable declarations, and the special $: syntax was limited to Svelte components. If you wanted to build some portable state you could define anywhere, and pass around, you’d use a store.

We won’t go through the whole API, but here’s a minimal example of a store in action. We’ll define a piece of state that holds a number, and, based on what that number is at anytime, spit out a label indicating whether the number is even or odd. It’s silly, but it should show us how stores work.

import { derived, writable } from "svelte/store";

export function createNumberInfo(initialValue: number = 0) {
  const value = writable(initialValue);

  const derivedInfo = derived(value, value => {
    return {
      value,
      label: value % 2 ? "Odd number" : "Even number",
    };
  });

  return {
    update(newValue: number) {
      value.set(newValue);
    },
    numberInfo: derivedInfo,
  };
}

Writable stores exist to write values to. Derived stores take one or more other stores, read their current values, and project a new payload. If you want to provide a mechanism to set a new value, close over what you need to. To consume a store’s value, prefix it with a $ in a Svelte component. It’s not shown here, but there’s also a subscribe method on stores, and a get import. If the store returns an object with properties, you can either “dot through” to them, or you can use a reactive assignment ($:) to get those nested values. The example below shows both, and this distinction will come up later when we talk about interoperability between Svelte 4 and 5.

<script lang="ts">
  import { createNumberInfo } from './numberInfoStore';

  let store = createNumberInfo(0);

  $: ({ numberInfo, update } = store);
  $: ({ label, value } = $numberInfo);
</script>

<div class="flex flex-col gap-2 p-5">
  <span>{$numberInfo.value}</span>
  <span>{$numberInfo.label}</span>
  <hr />
  <span>{value}</span>
  <span>{label}</span>

  <button onclick={() => update($numberInfo.value + 1)}>
    Increment count
  </button>
</div>

This was the old Svelte.

This is a post on the new Svelte, so let’s turn our attention there.

State in Svelte 5

Things are substantially simpler in Svelte 5. Pretty much everything is managed by something new called “runes.” Let’s see what that means.

Runes

Svelte 5 joins the increasing number of JavaScript frameworks that use the concept of signals. There is a new feature called runes and under the covers they use signals. These accomplish a wide range of features from state to props and even side effects. Here’s a good introduction to runes.

To create a piece of state, we use the $state rune. You don’t import it, you just use it — it’s part of the Svelte language.

let count = $state(0);

For values with non-inferable types, you can provide a generic

let currentUser = $state<User | null>(null);

What if you want to create some derived state? Before we did:

$: countTimes2 = count * 2;

In Svelte 5 we use the $derived rune.

let countTimes2 = $derived(count * 2);

Note that we pass in a raw expression. Svelte will run it, see what it depends on, and re-run it as needed. There’s also a $derived.by rune if you want to pass an actual function.

If you want to use these state values in a Svelte template, you just use them. No need for special $ syntax to prefix the runes like we did with stores. You reference the values in your templates, and they update as needed.

If you want to update a state value, you assign to it:

count = count + 1;
// or count++;

What about stores?

We saw before that defining portable state outside of components was accomplished via stores. Stores are not deprecated in Svelte 5, but there’s a good chance they’re on their way out of the framework. You no longer need them, and they’re replaced with what we’ve already seen. That’s right, the $state and $derived runes we saw before can be defined outside of components in top-level TypeScript (or JavaScript) files. Just be sure to name your file with a .svelte.ts extension, so the Svelte compiler knows to enable runes in these files. Let’s take a look!

Let’s re-implement our number / label code from before, in Svelte 5. This is what it looked like with stores:

import { derived, writable } from "svelte/store";

export function createNumberInfo(initialValue: number = 0) {
  const value = writable(initialValue);

  const derivedInfo = derived(value, value => {
    return {
      value,
      label: value % 2 ? "Odd number" : "Even number",
    };
  });

  return {
    update(newValue: number) {
      value.set(newValue);
    },
    numberInfo: derivedInfo,
  };
}

Here it is with runes:

export function createNumberInfo(initialValue: number = 0) {
  let value = $state(initialValue);
  let label = $derived(value % 2 ? "Odd number" : "Even number");

  return {
    update(newValue: number) {
      value = newValue;
    },
    get value() {
      return value;
    },
    get label() {
      return label;
    },
  };
}

It’s 3 lines shorter, but more importantly, much simpler. We declared our state. We computed our derived state. And we send them both back, along with a method that updates our state.

You may be wondering why we did this:

  get value() {
    return value;
  },
  get label() {
    return label;
  }

rather than just referencing those properties. The reason is that reading that state, at any given point in time, evaluates the state rune, and, if we’re reading it in a reactive context (like a Svelte component binding, or inside of a $derived expression), then a subscription is set up to update any time that piece of state is updated. If we had done it like this:

// this won't work
return {
  update(newValue: number) {
    value = newValue;
  },
  value,
  label,
};

That wouldn’t have worked because those value and label pieces of state would be read and evaluated right there in the return value, with those raw values getting injected into that object. They would not be reactive, and they would never update.

That’s about it! Svelte 5 ships a few universal state primitives which can be used outside of components and easily constructed into larger reactive structures. What’s especially exciting is that Svelte’s component bindings are also updated, and now support fine-grained reactivity that didn’t used to exist.

Props

Defining state inside of a component isn’t too useful if you can’t pass it on to other components as props. Props are also reworked in Svelte 5 in a way that makes them simpler, and also, as we’ll see, includes a nice trick to make TypeScript integration more powerful.

Svelte 4 props were another example of hijacking existing JavaScript syntax to do something unrelated. To declare a prop on a component, you’d use the export keyword. It was weird, but it worked.

// ChildComponent.svelte
<script lang="ts">
  export let name: string;
  export let age: number;
  export let currentValue: string;
</script>

<div class="flex flex-col gap-2">
  {name} {age}
  <input bind:value={currentValue} />
</div>

This component created three props. It also bound the currentValue prop into the <input>, so it would change as the user typed. Then to render this component, we’d do something like this:

<script lang="ts">
  import ChildComponent from "./ChildComponent.svelte";

  let currentValue = "";
</script>

Current value in parent: {currentValue}
<ChildComponent name="Bob" age={20} bind:currentValue />

This is Svelte 4, so let currentValue = '' is a piece of state that can change. We pass props for name and age, but we also have bind:currentValue which is a shorthand for bind:currentValue={currentValue}. This creates a two-way binding. As the child changes the value of this prop, it propagates the change upward, to the parent. This is a very cool feature of Svelte, but it’s also easy to misuse, so exercise caution.

If we type in the ChildComponent’s <input>, we’ll see currentValue update in the parent component.

Svelte 5 version

Let’s see what these props look like in Svelte 5.

<script lang="ts">
  type Props = {
    name: string;
    age: number;
    currentValue: string;
  };

  let { age, name, currentValue = $bindable() }: Props = $props();
</script>

<div class="flex flex-col gap-2">
  {name} {age}
  <input bind:value={currentValue} />
</div>

The props are defined via the $props rune, from which we destructure the individual values.

let { age, name, currentValue = $bindable() }: Props = $props();

We can apply typings directly to the destructuring expression. In order to indicate that a prop can be (but doesn’t have to be) bound to the parent, like we saw above, we use the $bindable rune, like this

 = $bindable()

If you want to provide a default value, assign it to the destructured value. To assign a default value to a bindable prop, pass that value to the $bindable rune.

let { age = 10, name = "foo", currentValue = $bindable("bar") }: Props = $props();

But wait, there’s more!

One of the most exciting changes to Svelte’s prop handling is the improved TypeScript integration. We saw that you can assign types, above. But what if we want to do something like this (in React)

type Props<T> = {
  items: T[];
  onSelect: (item: T) => void;
};
export const AutoComplete = <T,>(props: Props<T>) => {
  return null;
};

We want a React component that receives an array of items, as well as a callback that takes a single item (of the same type). This works in React. How would we do it in Svelte?

At first, it looks easy.

<script lang="ts">
  type Props<T> = {
    items: T[];
    onSelect: (item: T) => void;
  };

  let { items, onSelect }: Props<T> = $props();
  //         Error here _________^
</script>

The first T is a generic parameter, which is defined as part of the Props type. This is fine. The problem is, we need to instantiate that generic type with an actual value for T when we attempt to use it in the destructuring. The T that I used there is undefined. It doesn’t exist. TypeScript has no idea what that T is because it hasn’t been defined.

What changed?

Why did this work so easily with React? The reason is, React components are functions. You can define a generic function, and when you call it TypeScript will infer (if it can) the values of its generic types. It does this by looking at the arguments you pass to the function. With React, rendering a component is conceptually the same as calling it, so TypeScript is able to look at the various props you pass, and infer the generic types as needed.

Svelte components are not functions. They’re a proprietary bit of code thrown into a .svelte file that the Svelte compiler turns into something useful. We do still render Svelte components, and TypeScript could easily look at the props we pass, and infer back the generic types as needed. The root of the problem, though, is that we haven’t (yet) declared any generic types that are associated with the component itself. With React components, these are the same generic types we declare for any function. What do we do for Svelte?

Fortunately, the Svelte maintainers thought of this. You can declare generic types for the component itself with the generics attribute on the <script> tag at the top of your Svelte component:

<script lang="ts" generics="T">
  type Props<T> = {
    items: T[];
    onSelect: (item: T) => void;
  };

  let { items, onSelect }: Props<T> = $props();
</script>

You can even define constraints on your generic arg:

<script lang="ts" generics="T extends { name: string }">
  type Props<T> = {
    items: T[];
    onSelect: (item: T) => void;
  };

  let { items, onSelect }: Props<T> = $props();
</script>

TypeScript will enforce this. If you violate that constraint like this:

<script lang="ts">
  import AutoComplete from "./AutoComplete.svelte";

  let items = [{ name: "Adam" }, { name: "Rich" }];
  let onSelect = (item: { id: number }) => {
    console.log(item.id);
  };
</script>

<div>
  <AutoComplete {items} {onSelect} />
</div>

TypeScript will let you know:

Type '(item: { id: number; }) => void' is not assignable to type '(item: { name: string; }) => void'. Types of parameters 'item' and 'item' are incompatible.

Property 'id' is missing in type '{ name: string; }' but required in type '{ id: number; }'.

Effects

Let’s wrap up with something comparatively easy: side effects. As we saw before, briefly, in Svelte 4 you could run code for side effects inside of $: reactive blocks

$: {
  console.log(someStateValue1, someStateValue2);
}

That code would re-run when either of those values changed.

Svelte 5 introduces the $effect rune. This will run after state has changed, and been applied to the dom. It is for side effects. Things like resetting the scroll position after state changes. It is not for synchronizing state. If you’re using the $effect rune to synchronize state, you’re probably doing something wrong (the same goes for the useEffect hook in React).

The code is pretty anti-climactic.

$effect(() => {
  console.log("Current count is ", count);
});

When this code first starts, and anytime count changes, you’ll see this log. To make it more interesting, let’s pretend we have a current timestamp value that auto-updates:

let timestamp = $state(+new Date());
setInterval(() => {
  timestamp = +new Date();
}, 1000);

We want to include that value when we log, but we don’t want our effect to run whenever our timestamp changes; we only want it to run when count changes. Svelte provides an untrack utility for that

import { untrack } from "svelte";

$effect(() => {
  let timestampValue = untrack(() => timestamp);
  console.log("Current count is ", count, "at", timestampValue);
});

Interop

Massive upgrades where an entire app is updated to use a new framework version’s APIs are seldom feasible, so it should come as no surprise that Svelte 5 continues to support Svelte 4. You can upgrade your app incrementally. Svelte 5 components can render Svelte 4 components, and Svelte 4 components can render Svelte 5 components. The one thing you can’t do is mix and match within a single component. You cannot use reactive assignments $: in the same component that’s using Runes (the Svelte compiler will remind you if you forget).

Since stores are not yet deprecated, they can continue to be used in Svelte 5 components. Remember the createNumberInfo method from before, which returned an object with a store on it? We can use it in Svelte 5. This component is perfectly valid, and works.

<script lang="ts">
  import { createNumberInfo } from '../svelte4/numberInfoStore';

  const numberPacket = createNumberInfo(0);

  const store = numberPacket.numberInfo;
  let junk = $state('Hello');
</script>

<span>Run value: {junk}</span>
<div>Number value: {$store.value}</div>

<button onclick={() => numberPacket.update($store.value + 1)}>Update</button>

But the rule against reactive assignments still holds; we cannot use one to destructure values off of stores when we’re in Svelte 5 components. We have to “dot through” to nested properties with things like {$store.value} in the binding (which always works) rather than:

$: ({ value } = $store);

… which generates the error of:

$: is not allowed in runes mode, use $derived or $effect instead

The error is even clear enough to give you another alternative to inlining those nested properties, which is to create a $derived state:

let value = $derived($store.value);
// or let { value } = $derived($store);

Personally I’m not a huge fan of mixing the new $derived primitive with the old Svelte 4 syntax of $store, but that’s a matter of taste.

Parting thoughts

Svelte 5 has shipped some incredibly exciting changes. We covered the new, more reliable reactivity primitives, the improved prop management with tighter TypeScript integration, and the new side effect primitive. But we haven’t come closing to covering everything. Not only are there more variations on the $state rune, but Svelte 5 also updated it’s event handling mechanism, and even shipped an exciting new way to re-use “snippets” of HTML.

Svelte 5 is worth a serious look for your next project.

Article Series

]]>
https://frontendmasters.com/blog/introducing-svelte-5/feed/ 3 3067
How to use container queries now https://frontendmasters.com/blog/how-to-use-container-queries-now/ https://frontendmasters.com/blog/how-to-use-container-queries-now/#respond Tue, 09 Jul 2024 12:59:04 +0000 https://frontendmasters.com/blog/?p=2981 Philip Walton for Google responded to our question We’ve Got Container Queries Now, But Are We Actually Using Them? There are a variety of reasons why usage of them seems to be low, but certainly developers worried about browser support is still a factor. The biggest quoted concern is Safari 15, which is entirely reasonable.

Philip lays out a technique where you build a <responsive-container> Web Component which has a ResizeObserver that updates classes on itself you can use for fallback styles in browsers that don’t support @container.

It’s clever and efficient, but there is stuff I worry about:

  1. You can’t just update @media queries to @container queries with the same numbers, they measure very different things and will now break at different places.
  2. Every component is going to have different breakpoints, and now you need to keep those breakpoints synced in both HTML and CSS.
  3. Dealing with that style duplication is tricky. I would think a lot of setups don’t have a processing setup that has mixin abilities.

Still, if this helps you build a better system you can get to production, godspeed.

]]>
https://frontendmasters.com/blog/how-to-use-container-queries-now/feed/ 0 2981
Script Integrity https://frontendmasters.com/blog/script-integrity/ https://frontendmasters.com/blog/script-integrity/#respond Fri, 05 Jul 2024 16:22:36 +0000 https://frontendmasters.com/blog/?p=2923 There was lots of news recently about the polyfill.io project. This was a popular project a few years back, as you could just link it up and it would polyfill whatever web platform features it needed to based on feature testing and UA sniffing. If you loaded the script up from their domain, as the homepage suggested you did, you might have been served some malicious code.

Users are being redirected to sports betting websites or adult domains, likely based on their location, the threat intelligence firm said.

SecurityWeek

The project is offline now, but as of June 1st, 2024 this is how they suggested hotlinking.

When you link to any resource on a domain you don’t control, it’s a risk. It can be a calculated risk. For example, tons of websites load a script from google.com for their Google Analytics. Or load fonts from adobe.com for their typography. Those tend to be trusted sources, but that trust is entirely up to you.

It’s entirely possible that a resource you link to from a third-party disappears or changes. Worst case: changed maliciously, like we’ve seen here. You can bet your ass Google and Adobe don’t load any resources, especially JavaScript, from third-party domains they don’t control.

Protection Against Changes

There is a web platform feature that can help against a third party changing the code they are providing. It’s the integrity attribute on <script> or <link> elements (which are rel="stylesheet"rel="preload", or rel="modulepreload"). It’s called “Subresource Integrity”, to name it correctly.

So for example…

<script
  src="https://third-party.com/script.js"
  integrity="sha384-[hash value here]">
</script>

Now if script.js changes at all, even just a single character, the browser will refuse the execute the script (or stylesheet). Safety!

Some responsible third-parties will offer this directly, which is nice to see.

instant.page versions their script and provides the integrity attribute for safety.

CDNjs makes this a part of the default code you copy and paste.

I particularly like how the protection integrity provides protects against some possible middleman attacks as well. Say your weird hotel WiFi intercepts requests and changes the response (I’ve seen it!), this will prevent the tampered-with script from executing. Uh, well, unless they tamper with the HTML and change the attribute value, which is certainly possible as altering HTML is exactly what invasive in-app browsers do. Still, most security is a do-as-much-as-you-can game and this helps.

When Not to use Script Integrity

The above two examples are kinda perfect as the scripts they are linking to are versioned. They are published with that exact version and that version will never change. In this case, it’s by virtue of strong conventions. When public libraries update, the code at that version is locked in stone. Any changes cause a version update. If the code of a version changes without a version change, that would be highly suspicious, probably malicious, and a great situation for <script integrity="..."> to block. Plus, the major places where libraries are published (i.e. npm) literally don’t allow changes to published versions anyway.

While integrity is often an excellent idea, it’s specifically for versioned resources that don’t change. You would not use it if:

  1. You’re linking to a resource that is OK to change
  2. You’re providing a resource that you intend to change

Perhaps you’re using a script from some analytics service provider. It’s pretty likely that they don’t use the integrity attribute when they give you the script they want you to add. That’s likely because they want to be able to actively work on this script and what it does without having to tell every single customer they need to update the version of the script otherwise it will stop working.

Ironically, a company being able to update a script on the fly means they could potentially patch a security problem.

Would Script Integrity have Stopped the Polyfill.io Problem?

Maybe! It depends what they did. Some reporting said that:

The malicious code dynamically generates payloads based on HTTP headers, activating only on specific mobile devices, evading detection, avoiding admin users and delaying execution. The code is also obfuscated.

So: yes. If what was happening is that the initial response/content of the script was changed from what it was when the integrity value was created, it would have prevented these malicious changes from running.

But they could have evaded this kind of stoppage.

They way that polyfill.io worked already was that it loaded additional content — the polyfills themselves — as needed. That additionally-loaded content could have been changed to be malicious and would not have been subject to subresource integrity. I’m not trying to make bad guys lives easier here, just sayin’.

How To Do It Yourself

You don’t need a third-party to hand you this attribute to use it. It’s just a web platform feature, so you can use it if you want to.

Maybe the easier way is to go to the SRI Hash Generator website, pop in the URL of a resource you want to protect, and hit the button to get the code:

NOTE: I’ve seen plenty of places recommend this site, but when I used it, it didn’t seem to work for me. For example, the exact code above:

<script src="https://assets.codepen.io/3/log-something.js" integrity="sha384-ZTxYWn5UcuOi7Xt1wWg/QFcsZJEXjJg7tgCnVbx0+ssBvbi1Sw/hY5GzPGHSD1NW" crossorigin="anonymous"></script>

That fails for me in Chrome. I had to let Chrome error with a message in the console, which provides the correct hash in the error message, and use that corrected hash for it to work:

<script src="https://assets.codepen.io/3/log-something.js" integrity="sha384-H7W+IxM2qbwMSJYRqmcgYXq0TRko/BIFtURUjh2QG0Z8MM9I6t2f4n+2BOCmbjvD" crossorigin="anonymous"></script>

So… your milage may vary. Here’s the proof on all that.

It’s also worth noting that CORS is involved. Without that crossorigin="anonymous" on there, I was seeing a CORS error with that code above, even though we do serve assets with a Access-Control-Allow-Origin: * header. Shrug — websites are hard.

]]>
https://frontendmasters.com/blog/script-integrity/feed/ 0 2923
Primitive Obsession https://frontendmasters.com/blog/primitive-obsession/ https://frontendmasters.com/blog/primitive-obsession/#comments Thu, 04 Jul 2024 17:14:38 +0000 https://frontendmasters.com/blog/?p=2932 I found this essay Primitive Obsession by Andrew Cairns interesting. The big example in it is this:

$user = new User('abc123', 'email@address.com');

Here the User function takes two string params, string being a primitive type, and here, quite easy to screw up by passing them in the wrong order and/or not pre-validating the values. If you agree, there are a variety of ways to tackle this problem which generally boil down to passing in a typed object. TypeScript and Zod fans will be like: yeah duh.

]]>
https://frontendmasters.com/blog/primitive-obsession/feed/ 1 2932
New JavaScript Set methods https://frontendmasters.com/blog/new-javascript-set-methods/ https://frontendmasters.com/blog/new-javascript-set-methods/#respond Thu, 04 Jul 2024 14:14:48 +0000 https://frontendmasters.com/blog/?p=2928 Sets in JavaScript are quite useful as they are like an array, but can only contain one of a specific item. So you can unique-ify an array easily, and even willy-nilly add stuff to it and not worry about duplicates:

const people = ["chris", "chris", "cindy"];
const peopleSet = new Set(people);
peopleSet.add("chris");
console.log(peopleSet);
// {"chris","cindy"}

Now they are even more useful with recently universally available methods, as Brian Smith summarizes:

  • intersection() returns a new set with elements in both this set and the given set.
  • union() returns a new set with all elements in this set and the given set.
  • difference() returns a new set with elements in this set but not in the given set.
  • symmetricDifference() returns a new set with elements in either set, but not in both.
  • isSubsetOf() returns a boolean indicating if all elements of a set are in a specific set.
  • isSupersetOf() returns a boolean indicating if all elements of a set are in a specific set.
  • isDisjointFrom() returns a boolean indicating if this set has no elements in common with a specific set.
]]>
https://frontendmasters.com/blog/new-javascript-set-methods/feed/ 0 2928
YouTube Embeds are Bananas Heavy and it’s Fixable https://frontendmasters.com/blog/youtube-embeds-are-bananas-heavy-and-its-fixable/ https://frontendmasters.com/blog/youtube-embeds-are-bananas-heavy-and-its-fixable/#comments Mon, 01 Jul 2024 12:56:07 +0000 https://frontendmasters.com/blog/?p=2881 TL;DR: YouTube Embeds are like 1.3MB in size with no shared resources between multiple embeds. Using a <lite-youtube> Web Component is more like 100k, does share resources, and sacrifices no functionality.

You can put a YouTube video on any website. They help you do it. Under the Share menu right on youtube.com there is an option to <> Embed and you’ll see bit of HTML with an <iframe> in it.

<iframe>s are never wonderful for performance, but they make sense for protected third-party content.

This is what I’m getting as I write:

<iframe 
  width="560" 
  height="315" 
  src="https://www.youtube.com/embed/LN1TQm942_U?si=EfW_M4bEHEO-idL3"
  title="YouTube video player"
  frameborder="0"
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
  referrerpolicy="strict-origin-when-cross-origin"
  allowfullscreen>
</iframe>

If I were Team YouTube, I’d get loading="lazy" on there to help with performance right away. No need for videos that aren’t even visible on the page to load right away.

<iframe 
  ...
  loading="lazy"
  >
</iframe>

Plus I’d put some inline styles on there to keep the video fluid and maintain the original aspect ratio. Or you could target these and do that yourself in CSS. Here’s assuming the videos are the standard 16 / 9 aspect ratio:

iframe[src^="https://www.youtube.com/embed/"] {
  inline-size: 100%;
  block-size: auto;
  aspect-ratio: 16 / 9;
}

But… let’s not keep this HTML at all. I’m sure you read this blog post title, but let’s put a point on it:

On a page with literally nothing at all on it other than a YouTube Embed, we’re looking at:

  • 32 requests
  • 1.3 MB of data transfer
  • 2.76s to load the page on my current WiFi connection

Zach Leatherman, equally exasperated by this, noted:

The weight also grows linearly with every embed—resources are not shared: two embeds weigh 2.4 MB; three embeds weigh 3.6 MB (you get the idea).

Wow.

Looks like sizes are up a bit since Zach last looked as well.

The Appearance & Functionality

This is what you get from a YouTube Embed:

  • You see a “poster” image of the video
  • You see the title of the video
  • You see a big play button — click it to play the video

This is very little UI and functionality, which is fine! We can absolutely do all this without this many resources.

Why is it this way? 🤷‍♀️

I don’t think we have any good answers here. In fact, I heard from a little birdie who ran it up the pole that they have tested lighter embeds and found them to reduce engagement. 😭

I’m just gonna straight up say I don’t believe it. It’s like when Google told us that taking up half the screen with AI generated answers led to people clicking on third-party results more, but then refused to show data or allow us to track those clicks ourselves.

And hey — sometimes there are unexpected results in testing. That’s why we test instead of guess. But because this is so counterintuitive and offtrack for so many other similar performance testing situations, this bears deeper scrutiny. It would benefit from an opening of the methodology and data.

Like if you tell me that if you hit people with a stick and they smile more, I’m gonna want you to stop until we can look at what’s going on there.

I really wish I could find a good link for this, but there is a famous story from YouTube engineers way-back-when who made a much lighter video page and put it into testing. They found, quite counterintuitively, that average page load times went up. But with a deeper look, they found that the lighter page was able to reach more people, including people on low-power low-internet-speed devices who were able to actually use YouTube for the first time, and them using it much more slowed those averages. That’s awesome! The speed of using the site was up relatively for everyone. The metric of the average page load speed was a red herring and ultimately not meaningful.

How do we know that’s not the same kind of thing happening here?

Remember the implications of all these resources isn’t just a little inconvenience. YouTube is so enormous we’re talking incredible amounts of wasted electricity and thus carbon output. Pulling a megabyte of data off every single YouTube Embed would be an incredible win all around. I might even say not improving this is environmentally negligent.

The Solution is to Replicate the Embed Experience Another Way. There are Open Source Web Components That Do It Well.

With a little dab of irony, Google’s own performance champion Paul Irish has had a web component doing just this for years and years and years:

lite-youtube-embed

The pitch is solid:

Provide videos with a supercharged focus on visual performance. This custom element renders just like the real thing but approximately 224× faster.

Two hundred and twenty four times faster. Which of course involves much less data transfer.

And I’d like to be very clear, also does the exact same thing as the default embed:

  • You see a “poster” image of the video
  • You see the title of the video
  • You see a big play button — click it to play the video

You lose nothing and gain tons of speed, efficiency, and default privacy.

Using Lite YouTube Embed

  1. Link up the JavaScript to instantiate the Web Component
  2. Use it

You could install it from npm or copy and paste a copy into your own project or whatever. Or link it from a CDN:

import "https://esm.sh/lite-youtube-embed";

That’s like this:

But the best way to use it is right in the README:

Use this as your HTML, load the script asynchronously, and let the JS progressively enhance it.

<script defer src="https://cdnjs.cloudflare.com/ajax/libs/lite-youtube-embed/0.3.2/lite-yt-embed.js"></script>

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lite-youtube-embed/0.3.2/lite-yt-embed.css" integrity="sha512-utq8YFW0J2abvPCECXM0zfICnIVpbEpW4lI5gl01cdJu+Ct3W6GQMszVITXMtBLJunnaTp6bbzk5pheKX2XuXQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />

<lite-youtube videoid="ogfYd705cRs" style="background-image: url('https://i.ytimg.com/vi/ogfYd705cRs/hqdefault.jpg');">
  <a href="https://youtube.com/watch?v=ogfYd705cRs" class="lty-playbtn" title="Play Video">
    <span class="lyt-visually-hidden">Play Video: Keynote (Google I/O '18)</span>
  </a>
</lite-youtube>

With async loaded JavaScript, note the background-image is put into the HTML so it can all look right before the JavaScript loads.

Alternatives

]]>
https://frontendmasters.com/blog/youtube-embeds-are-bananas-heavy-and-its-fixable/feed/ 11 2881