The article has been originally posted on my Polish blog —

If you have any experience with JavaScript, you have probably used listeners. Listeners, as the name says, listen to a particular event or events. After the event occurrence, they execute previously defined actions. For those who have never had a chance to use JavaScript listeners, let’s take a look at the sample code:

The code passed to callback will execute, every single time when an element with an id equals foo will be clicked. Of course, there are much more events than click. For the full list of events, visit MDN.

Similarly, the observer Web APIs provided by browsers work. Before I start describing all the observers I need to explain some theory. As you can see I still write about observers, observing, and so on. This concept is described and defined in the programming world as the observer design pattern.

A few words about the observer

As the name may suggest, the core element of this pattern is the observer and observables. It may sound weird but shortly: the observer observes observables and execute a previously defined action when some declared condition will occur. For example, in our neighborhood lives an annoying neighbor who doesn’t like children. This neighbor tries to chase away children every time when children play football next to his house. The neighbor is the observer, and the children are observables. When the children start playing football, the observer executes a previously defined action - he tries to chase the children away or call the police. It depends on how annoying the neighbor is 😄

There are many more use cases of this pattern in daily life. If you subscribe to a channel on YouTube you can click a bell icon and receive a notification when a new video will be added. So you create an observer that notifies you when a particular action will be done — a new video release. This mechanism is implemented also in almost every mobile app!

JavaScript observers

When you create a browser app in JavaScript, a few observer Web APIs are provided:

  • MutationObserver
  • PerformanceObserver
  • ResizeObserver
  • IntersectionObserver


As the name says, the MutationObserver listens to mutations (changes) in the observed object. To clarify it, let’s take a look at the example:

At first glance, this code doesn’t seem to be complicated. First, a new observer is created, where we declare an action that will be executed when the event will be observed. Then, the observe() method is called. The element with the foo id is added to the observables pool with some additional parameters that allow specifying whether e.g. children or attributes should be observed as well.

The first option is related to attributes change. The example attributes are class, id, href, checked, etc. However, it doesn’t affect data-value attributes. For data attributes there’s a dedicated option — characterData. Another option is childList that listens to adding or removing children. Last but not least is the subtree option, that causes all children are observed too.

Here, I need to point out these options should be used carefully. For example, if you create a mutation observer on the body element it may bring a huge impact on performance. In this case, every single change in the DOM tree will trigger the defined action! Besides these options, there are also some other options to set:

  • attributeOldValue — defines whether old attribute value should be passed to observer callback,
  • characterDataOldValue — same as the previous one but related to data attributes,
  • attributeFilter — allows specifying an array of attributes to observe. It doesn't make any sense to observe all attributes when it is not needed.

Do not forget to disable the observer when it is not needed anymore using the observer.disconnect() method.


I mentioned the performance factor in this article once. For measuring performance, the PerformanceObserver can be used. In this case, a piece of code may be useful too:

As you can see, creating other observers looks similar. As the argument, a callback with defined action is passed. In the observe() method a list of entryTypesis defined. In this case only the measure event is handled. The above code will measure how much time will take adding numbers from 0 to 50000. Each occurrence of the performance.measure('sum') call, is added to the entries array. Then, it is easy to calculate a time difference between each occurrence. The full list of available entryTypes is available on MDN.


When I heard about the ResizeObserver for the first time I had some doubts about it. I can use the resize event, that is emitted on the window or the document size change. The game-changer is that ResizeObserver allows listening to size change for any element on the website.

It includes also changes caused by adding or removing elements to the observed element. What’s more, we don’t have to look for element dimensions in its properties — the information is available in the callback directly:

Using this observer is trivial and there’s no difference in comparison with others. We just create a new instance and execute the observe() method with the observed element inside.


Last but not least is the IntersectionObserver. The use case of this observer is when you need to check whether an observed element is physically visible on the browser view. It can be used for example for lazy loading or infinite scrolling features. The code for this observer is identical to the previous but I’ll add it anyway:

The interesting property of theentry object is intersectionRatio that says what percentage of the observed object is visible in the browser view.