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
show(), hide(), addClass(), css()
)jQuery makes many checks for legacy browsers we don't care about anymore:
https://github.com/jquery/jquery/blob/354f6036f251a3ce9b24cd7b228b4c7a79001520/src/css/support.js Show archive.org snapshot
jQuery wraps native functionality with a lot of code:
https://github.com/jquery/jquery/blob/master/src/event.js Show archive.org snapshot
Method | duration | |
---|---|---|
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 |
Method | duration |
---|---|
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.
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.
So you get a feel how working without jQuery looks like.
// jQuery
let $cards = $('.card')
let $firstCard = $('.card:first')
// Native
let cards = document.querySelectorAll('.card')
let firstCard = document.querySelector('.card')
// jQuery
let $form = $('form')
let $fields = $form.find('input, textarea, select')
// Native
let form = document.querySelector('form')
let fields = form.querySelectorAll('input, textarea, select')
// 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!
// jQuery
let $button = $('button')
let isButtonActive = $button.is('.active')
// Native
let button = document.querySelector('button')
let isButtonActive = button.matches('.active')
// 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.
// 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'
// 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')
// jQuery
let $div = $('<div></div>')
$div.appendTo(document.body)
// Native
let div = document.createElement('div')
document.body.appendChild(div)
// jQuery
let listener = (event) => { ... }
element.on('click', listener)
element.off('click', listener)
// Native
let listener = (event) => { ... }
element.addEventListener('click', listener)
element.removeEventListener('click', listener)
In our apps the same element often has three layers of wrapping:
let element = ..
let $element = $(element)
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
<tag>
to a browserHTMLElement
, but can't possible wrap custom element API.$.Event
.A good guide with before/after code is You Don't Need jQuery Show archive.org snapshot .
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.
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 Show archive.org snapshot (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()
:
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+:
if (!Element.prototype.closest) {
Element.prototype.closest = function(selector) {
return up.element.closest(this, ...args);
}
}
Libary | ||
---|---|---|
jQuery | 86.9 kB minified | 30,4 kB gzipped |
dom4 | 10.0 kB minfied | 4.3 kB gzipped |
up.element + up.event | 16 kB minfied | 5 KB gzipped |
UmbrellaJS | 8.0 kB minfied | 2.5 kB gzipped |
up.element
A new, experimental up.element
module offers functions for DOM manipulation and traversal.
It complements
native Element
methods
Show archive.org snapshot
and works across all supported browsers.
Method | Description |
---|---|
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 Show archive.org snapshot . |
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 Show archive.org snapshot . |
up.element.setTemporaryStyle() |
Temporarily sets the inline CSS styles on the given element. |
up.element.style() |
Receives computed CSS styles Show archive.org snapshot for the given element. |
up.element.styleNumber() |
Receives a computed CSS property value Show archive.org snapshot 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 Show archive.org snapshot that matches all elements that have a descendant matching the given selector. |
Instead use up.compiler()
or
if (document.readyState != 'loading')
callback()
else
document.addEventListener('DOMContentLoaded', callback)
This is something where jQuery really shines:
$('<div>foo</div>').css('color': 'red').attr('up-target', 'page').appendTo('body')
Instead use more code or up.element.affix()
:
up.element.affix(document.body, 'div[up-target=page]', style: { color: 'red' })
Like "input:focus", "div:visible", ".form-group:has(.email)"
.
Instead filter results of querySelectorAll()
in JavaScript.
up.element
supports :has()
:
let formsWithButton = up.element.all('form:has(button)')
$.ajax()
Instead use standard
fetch()
Show archive.org snapshot
or up.request()
.
$.Deferred()
Instead use native Promise
.
Use CSS Transitions:
function fadeIn(element) {
element.style.opacity = '0'
element.style.transition = 'opacity 1s ease'
element.style.opacity = '1'
}
jQuery collections automatically apply an operation to all elements in the collection:
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()
.
jQuery collections automatically no-op on empty collections:
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.
#sos
resource_controller
(but not a Rails upgrade).$
. This will show you all the affected lines of code.up.element.get()
helps)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:
up.compiler()
callback now receives a
native element
Show archive.org snapshot
instead of a jQuery collection. For the old behavior, use up.$compiler()
.up.macro()
callback now received a
native element
Show archive.org snapshot
instead of a jQuery collection. For the old behavior, use up.$macro()
.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()
Show archive.org snapshot
. You may continue to use jQuery's
jQuery#on()
Show archive.org snapshot
to listen to Unpoly events, but you need to access custom properties from event.originalEvent
.
up.compiler()
, use up.$compiler()
up.macro()
, use up.$macro()
up.on()
, use up.$on()