Posted 11 months ago. Visible to the public.

JavaScript without jQuery (presentation from 2019-01-21)

Summary

  • We want to move away from jQuery in future projects
  • Motivations are performance, bundle size and general trends for the web platform.
  • The native DOM API is much nicer than it used to be, and we can polyfill the missing pieces
  • Unpoly 0.60.0 works with or without jQuery

Is jQuery slow?

Copy
From: Sven To: unpoly@googlegroups.com Subject: performance on smartphones and tablets Hello I just used your framework in one project and must say, I am really pleased with it -- but only on a desktop computer. Have you benchmarked the framework also on smartphones and tablets? Unfortunately I must say, that it does not feel snappy at all on mobile devices. Actually very often it feels much slower, than without unpoly. This defies it's purpose. And it is nowhere on the page stated, that it is only snappy on a computer with a powerful cpu, as the JavsScript it otherwise to slow. As this is probably a known issue, is the framework only meant to be used for desktop pc's and not for mobile devices? The problem has nothing to do with the performance of my own site, as the unpoly site itself feels very laggy on my mobile devices -- on a smartphone and on a tablet (no super highend devices, but neither very slow ones). Kind regards Sven

The peformance impact of removing jQuery (with pictures)

Unpoly 0.37

  • jQuery DOM manipulation
  • jQuery AJAX
  • jQuery Promises

Image

Unpoly 0.50.0

  • jQuery DOM manipulation
  • Native AJAX
  • Native Promises

Image

Unpoly 0.56.0

  • jQuery DOM manipulation
  • Optimized as far as possible without removing jQuery entirely
  • Many slow jQuery functions replaced (show(), hide(), addClass(), css())

Image

Unpoly 0.60.0

  • jQuery removed.
  • Everything is native.

Image

What makes jQuery burn so many cycles?

jQuery makes many checks for legacy browsers we don't care about anymore:

https://github.com/jquery/jquery/blob/354f6036f251a3ce9b24cd7b228b4c7a79001520/src/css/support.js

jQuery wraps native functionality with a lot of code:

https://github.com/jquery/jquery/blob/master/src/event.js

Overhead of adding and removing 1000 event listeners

Element#addEventListener() 11.3 ms native code
$.fn.on() 60.3 ms ~ 1000 lines of userland code
up.on() 15.9 ms ~ 130 lines of userland code

Overhead of adding 200 <div> elements to an Array vs. adding to a jQuery collection

Array.prototype.push() 0 ms
$.fn.add() 28 ms

… Because jQuery collections are always in DOM order.

In general jQuery trades correctness in all edge cases and legacy browsers for performance.

jQuery parse time

See my earlier presentation about parse time impact.

jQuery is 86.9 kB minified, 30.4 kB gzipped.

Removing jQuery makes your app boot some seconds faster on slow devices, or frees up that budget for other libraries.

Working the DOM without jQuery (basics)

So you get a feel how working without jQuery looks like.

Selecting elements in the document

Copy
// jQuery let $cards = $('.card') let $firstCard = $('.card:first') // Native let cards = document.querySelectorAll('.card') let firstCard = document.querySelector('.card')

Selecting descendants

Copy
// jQuery let $form = $('form') let $fields = $form.find('input, textarea, select') // Native let form = document.querySelector('form') let fields = form.querySelectorAll('input, textarea, select')

Find closest ancestor or self

Copy
// jQuery let $input = $('input') let $form = $input.closest('form') // Native let field = document.querySelector('input') let form = field.closest('form')

jQuery has influenced the native API a lot!

Check if element matches selector

Copy
// jQuery let $button = $('button') let isButtonActive = $button.is('.active') // Native let button = document.querySelector('button') let isButtonActive = button.matches('.active')

Read and write attributes

Copy
// jQuery let name = $element.attr('name') $element.attr('name', 'new-name') // Native let name = element.getAttribute('name') element.setAttribute('name', 'new-name') // ... but you probably want to work with properties.

Read and write properties (!)

Copy
// jQuery let value = $input.prop('value') $element.prop('value', 'foo@bar.com') // Native let name = input.name input.name = 'foo@bar.com' // jQuery let src = $image.prop('src') $element.prop('src', 'foo.png') // Native let src = image.src input.src = 'foo@.png'

Working with CSS class names

Copy
// jQuery $input.hasClass('active') $input.addClass('active') $input.removeClass('active') $input.toggleClass('active') // Native input.classList.contains('active') input.classList.add('active') input.classList.remove('active') input.classList.toggle('active')

Creating a new element

Copy
// jQuery let $div = $('<div></div>') $div.appendTo(document.body) // Native let div = document.createElement('div') document.body.appendChild(div)

Event listeners

Copy
// jQuery let listener = (event) => { ... } element.on('click', listener) element.off('click', listener) // Native let listener = (event) => { ... } element.addEventListener('click', listener) element.removeEventListener('click', listener)

The Web platform moves away from wrapping

In our apps the same element often has three layers of wrapping:

  1. The native DOM element javascript let element = ..
  2. The native element wrapped as a jQuery collection: javascript let $element = $(element)
  3. An API object to call additional JavaScript behavior added by a library: javascript var player = flowplayer($element) player.play()

Framework activation layers (like Angular directives, Unpoly compilers, React components, $.unobtrusive()) are all layer 3.

It's simpler to just have one layer: The native DOM element is its API

https://makandracards.com/makandra/62353-javascript-without-jquery-presentation-from-2019-01-21/attachments/7225

The future is Custom Elements

  • In the future library developers will no longer ship JavaScript as Angular directives, Unpoly compilers or anything framework-specific. It will all be Custom Elements.
  • Custom elements let you teach a new <tag> to a browser
  • The browser will automatically pair JavaScript and HTML when a matching element enters the DOM. No JavaScript activation required.

https://makandracards.com/makandra/62353-javascript-without-jquery-presentation-from-2019-01-21/attachments/7224

JavaScript libraries are already shipping as custom elements

  • For example Trix from Basecamp (integrated as ActionText in Rails 6).
  • By choosing custom elements as their delivery method, they work in all JavaScript frameworks or without any framework.

jQuery vs. Custom Elements

  • jQuery has wrapped access to HTMLElement, but can't possible wrap custom element API.
  • To interact with custom elements, we need.
  • Same for new event types and properties that would need to be manually wrapped by $.Event.

Translating your jQuery habbits to native API

A good guide with before/after code is You Don't Need jQuery.

Note that some guides still hold compatibility to IE9/10, and we don't need it to support IE11!

Everything will become even easier when we kill IE11 for most apps in Feb 2020.

Supporting IE11

IE11 supports much, but not all of the DOM traversal/manipulation functions.

There are three functions that I miss daily in IE11:

  • Element#closest() (not supported in IE11)
  • Element#matches() (is Element#msMatches() in IE11)
  • Element#toggleClass(klass, newState) (second argument not supported in IE11)
  • NodeList#forEach() (not supported in IE11)

There are millions of Polyfills that will normalize DOM API support across all browsers, e.g. dom4 (10 kB minfied, 4.3 kB gzipped).

But just to take the magic out of this, here is a homegrown polyfill e.g. for Element#closest():

Copy
if (!Element.prototype.closest) { Element.prototype.closest = function(selector) { if (this.matches(selector)) { return this; } else if (this.parentElement) { this.parentElement.closest(selector) } } }

… or, if you have Unpoly 0.60+:

Copy
if (!Element.prototype.closest) { Element.prototype.closest = function(selector) { return up.element.closest(this, ...args); } }

DOM helper libs compared

jQuery 86.9 kB minified 30,4 kB gzipped  
dom4 10.0 kB minfied 4.3 kB gzipped repo
up.element + up.event 16 kB minfied 5 KB gzipped you might already have it
UmbrellaJS 8.0 kB minfied 2.5 kB gzipped docs code

About up.element

A new, experimental up.element module offers functions for DOM manipulation and traversal.

It complements native Element methods and works across all supported browsers.

up.element.first() Returns the first descendant element matching the given selector.
up.element.all() Returns all descendant elements matching the given selector.
up.element.subtree() Returns a list of the given parent's descendants matching the given selector. The list will also include the parent element if it matches the selector itself.
up.element.closest() Returns the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.
up.element.matches() Matches all elements that have a descendant matching the given selector.
up.element.get() Casts the given value to a native Element.
up.element.remove() Removes the given element from the DOM tree.
up.element.toggle() Display or hide the given element, depending on its current visibility.
up.element.toggleClass() Adds or removes the given class from the given element.
up.element.hide() Hides the given element.
up.element.show() Shows the given element.
up.element.setAttrs() Sets all key/values from the given object as attributes on the given element.
up.element.replace() Replaces the given old element with the given new element.
up.element.createFromSelector() Creates an element matching the given CSS selector.
up.element.setAttrs() Sets all key/values from the given object as attributes on the given element.
up.element.affix() Creates an element matching the given CSS selector and attaches it to the given parent element.
up.element.toSelector() Returns a CSS selector that matches the given element as good as possible.
up.element.createFromHtml() Creates an element from the given HTML fragment.
up.element.booleanAttr() Returns the value of the given attribute on the given element, cast as a boolean value.
up.element.numberAttr() Returns the value of the given attribute on the given element, cast to a number.
up.element.jsonAttr() Reads the given attribute from the element, parsed as JSON.
up.element.setTemporaryStyle() Temporarily sets the inline CSS styles on the given element.
up.element.style() Receives computed CSS styles for the given element.
up.element.styleNumber() Receives a computed CSS property value for the given element, casted as a number.
up.element.setStyle() Sets the given CSS properties as inline styles on the given element.
up.element.isVisible() Returns whether the given element is currently visible.
:has() A non-standard pseudo-class that matches all elements that have a descendant matching the given selector.

What jQuery developers are going to miss

Run code on page load

Instead use up.compiler() or

Copy
if (document.readyState != 'loading') callback() else document.addEventListener('DOMContentLoaded', callback)

Fast element construction

This is something where jQuery really shines:

Copy
$('<div>foo</div>').css('color': 'red').attr('up-target', 'page').appendTo('body')

Instead use more code or up.element.affix():

Copy
up.element.affix(document.body, 'div[up-target=page]', style: { color: 'red' })

Non-standard pseudo-classes

Like "input:focus", "div:visible", ".form-group:has(.email)".

Instead filter results of querySelectorAll() in JavaScript.

up.element supports :has():

Copy
let formsWithButton = up.element.all('form:has(button)')

$.ajax()

Instead use standard fetch() or up.request().

$.Deferred()

Instead use native Promise.

JavaScript Animations

Use CSS Transitions:

Copy
function fadeIn(element) { element.style.opacity = '0' element.style.transition = 'opacity 1s ease' element.style.opacity = '1' }

No automatic iteration

jQuery collections automatically apply an operation to all elements in the collection:

Copy
let $divs = $('div') $divs.css({ color: 'red' }) // changes all divs

This is sometimes practical, but sometimes hides bugs when a function
built for a single element has undefined (but error-free) behavior
when called for multiple elements.

Instead use for or Array#forEach().

No automatic null-Object

jQuery collections automatically no-op on empty collections:

Copy
let $emptyCollection = $([]) $emptyCollection.css({ color: 'red' }) // does nothing. throws no error.

This is sometimes practical, but in my own code this mostly hides bugs
and delays crashes that could have happened earlier.

Recommendation for our projects

New projects

  • New projects should not include jQuery
  • Use a Polyfill like dom4 to normalize browser support
  • Use up.element, Umbrella.js oder even jQuery if you run into convenience issues
  • Hotline #sos

Existing projects

  • For existing projects with lots of jQuery code, it's up to the project lead to decide whether to removing jQuery is a long-term goal for you
  • If you're unsure:
    • Consider writing new functions with the native DOM API
    • Consider not using jQuery in the public signature of new functions
  • If you want to migrate your project now
    • Talk to your client about alotting time. This is a refactoring similar to removing resource_controller (but not a Rails upgrade).
    • Grep for $. This will show you all the affected lines of code.
    • Refactor functions to accept both jQuery and Elements (up.element.get() helps)
    • Remove jQuery from callers one at a time.
      • This is mostly repetitious work.
      • We will practice this below.
    • When all callers are changed, simplify receiving functions to only accept native elements

Unpoly 0.60.0 will work without jQuery

Starting from 0.60.0 (release this week), jQuery is no longer required to use Unpoly. Unpoly no longer has any dependencies.

Effort has been made to ensure that migrating to this version is smooth for existing apps that use jQuery.

All Unpoly functions that accept element arguments will accept both native elements and jQuery collections.

You will need to prefix some function calls with $ to have your callbacks called with jQuery collections instead of native elements:

  • The up.compiler() callback now receives a native element instead of a jQuery collection. For the old behavior, use up.$compiler().
  • The up.macro() callback now received a native element instead of a jQuery collection. For the old behavior, use up.$macro().
  • The event handler passed to up.on() now receives an element instead of a jQuery collection. For the old behavior, use up.$on().

Finally, all Unpoly events (up:*) are now triggered as native events that can be received with Element#addEventListener(). You may continue to use jQuery's jQuery#on() to listen to Unpoly events, but you need to access custom properties from event.originalEvent.

Exercise: Migrating to Unpoly 0.60 while removing jQuery

  • Show how to remove jQuery from unpoly-guide
  • Start with b496388be0d5c995ca60badf45f7acdbb76b284b
  • Just grep for "$" and rewrite occurences like a robot

Exercise: Migrating to Unpoly 0.60 while keeping jQuery

  • Show how to Upgrade Cards to Unpoly 0.60 while keeping all our jQuery code
  • Start with 8002f3ce18f5405c87b68174871a520d03fd3613
  • instead of up.compiler(), use up.$compiler()
  • instead of up.macro(), use up.$macro()
  • instead of up.on(), use up.$on()

Thank you!

By refactoring problematic code and creating automated tests, makandra can vastly improve the maintainability of your Rails application.

Owner of this card:

Avatar
Henning Koch
Last edit:
10 months ago
by Henning Koch
Attachments:
0.37.0.png, 0.50.0.png, 0.56.0.png, 0.60.0.png, custom-element-timer.html, video-element.html
About this deck:
We are makandra and do test-driven, agile Ruby on Rails software development.
License for source code
Posted by Henning Koch to makandra dev
This website uses cookies to improve usability and analyze traffic.
Accept or learn more