In the past I've also used a resize listener that bundled requestAnimationFrame with it to be a somewhat optimized version of polling resize events:
/**
* Resize listener
* #return {function}
*/
export const optimizedResize = (function () {
let callbacks = [],
running = false;
// fired on resize event
function resize() {
if (!running) {
running = true;
if (window.requestAnimationFrame) {
window.requestAnimationFrame(runCallbacks);
} else {
setTimeout(runCallbacks, 66);
}
}
}
// run the actual callbacks
function runCallbacks() {
callbacks.forEach((callback) => {
callback();
});
running = false;
}
// adds callback to loop
function addCallback(callback) {
if (callback) {
callbacks.push(callback);
}
}
return {
// public method to add additional callback
add(callback) {
if (!callbacks.length) {
window.addEventListener('resize', resize);
}
addCallback(callback);
},
};
}());
I recently came across addListener() which embarrassingly I must say I'm not familiar with. Although it says it's just an alias for addEventListener() the syntax seems pretty straight forward to listen to changes:
const viewportMediumMin = window.matchMedia(`(min-width: 768px)`);
viewportMediumMin.addListener(checkScreenSize);
But, what I'm trying to figure out is addListener() is the equivalent of:
window.addEventListener('resize', function() {
console.log('addEventListener - resize');
}, true);
or if it's doing something "smarter" behind the scenes that I should rely on it exclusively, compared to the optimizedResize method I mentioned. I'm really only interested in the specific event of the media query changing, not finding out every single pixel width change. Thanks for any help!
This is basically reinventing wheels. CSS3 was made to style the pages content gracefully on different screen sizes, and media queries were added to change the look of a page at a certain breakpoint. Those breakpoints are common in todays web development, and modern CSS engines were heavily optimized to perform changes as fast as possible. Therefore
window.matchMedia(`(min-width: 768px)`).addListener(/*...*/)
is probably very performant as the event is detected by the CSS engine that then gets redirected to the JS engine. Adding a listener to resize is probably slower as every pixel change causes a JS event, and your unoptimized JavaScript has to figure out wether a breakpoint was passed. Your optimizeResize doesn't really change that.
Related
I have a React application that uses a data visualization library that uses PixiJS.
I occasionally get frustrating CONTEXT_LOST_WEBGL errors in Chrome that force the user to manually reload the page, in order for the page to be (re)rendered.
I cannot often or reliably reproduce the error, but I know that it happens as other people tell me the application occasionally shows no data. The situations that raise this error seem very context-dependent and therefore difficult to recapitulate — low-powered graphics adapters, or lots of tabs open at once, etc.
The end user would only know that there are CONTEXT_LOST_WEBGL errors if that user has the Developer Tools console window open. Otherwise, the web page just looks blank.
I have tried the following to set up my React application to reload the window without manual user intervention, when a webglcontextlost event occurs:
componentDidMount() {
...
window.addEventListener("webglcontextlost", (e) => { window.location.reload(); });
...
}
I'm not sure it is working correctly, i.e., if the webglcontextlost event is being handled elsewhere. Or perhaps I am trying to subscribe to the wrong event?
Otherwise, to try to handle this more gracefully, is there a way in raw Javascript, or via a third-party library, to periodically measure available memory for WebGL, and to use that measurement to instead reload the page, when the available memory reaches some arbitrary threshold that might predict an imminent CONTEXT_LOST_WEBGL error condition?
is there a way in raw Javascript to periodically measure available memory for WebGL
No, just as there is no way to measure JavaScript memory
window.addEventListener("webglcontextlost", (e) => { window.location.reload(); });
Is wrong. It should be
someCanvas.addEventListener("webglcontextlost", (e) => { window.location.reload(); });
Each canvas can individually lose its context. Most browsers only allow 8 to 16 WebGL contexts at once. As soon as the limit is reached canvases start to lose their contexts.
As for recovering gracefully it's a lot of work. Basically you need recreate all WebGL resources which means you need to structure your code so that's posssible. Separate all the state of your app from the stuff releated to WebGL (or from pixi.js) and when you get a context lost event then prevent default and recreate all the WebGL stuff
let gl;
someCanvas.addEventListener("webglcontextlost", (e) => {
e.preventDefault(); // allows the context to be restored
});
someCanvas.addEventListener("webglcontextrestored", (e) => {
initWebGL(gl);
});
gl = someCanvas.getContext('webgl');
initWebGL(gl);
Note that pixi.js itself may or may not be designed to handle contextlost
The following code helped restart my Pixijs web application, when the WebGL context is lost:
addCanvasWebGLContextLossEventListener = () => {
const canvases = document.getElementsByTagName("canvas");
if (canvases.length === 1) {
const canvas = canvases[0];
canvas.addEventListener('webglcontextlost', (event) => {
window.location.reload();
});
}
}
removeCanvasWebGLContextLossEventListener = () => {
const canvases = document.getElementsByTagName("canvas");
if (canvases.length === 1) {
const canvas = canvases[0];
canvas.removeEventListener('webglcontextlost');
}
}
For other applications with more than one canvas, some adjustments would be needed to add other listeners.
The following code helped me simulate a lost context condition (and to restore from it, via the webglcontextlost event):
simulateWebGLContextLoss = () => {
//
// simulate loss of WebGL context, for the purposes
// of improving user experience when the browser is
// overwhelmed
//
const canvases = document.getElementsByTagName("canvas");
if (canvases.length === 1) {
setTimeout(() => {
const canvas = canvases[0];
const webgl2Context = canvas.getContext("webgl2", {});
if (webgl2Context) {
console.log(`losing webgl2 context...`);
webgl2Context.getExtension('WEBGL_lose_context').loseContext();
}
else {
const webglContext = canvas.getContext("webgl", {});
if (webglContext) {
console.log(`losing webgl context...`);
webglContext.getExtension('WEBGL_lose_context').loseContext();
}
}
}, 5000);
}
}
For React lifecycle setup:
componentDidMount() {
setTimeout(() => {
this.addCanvasWebGLContextLossEventListener();
}, 2500);
}
componentWillUnmount() {
this.removeCanvasWebGLContextLossEventListener();
}
A timeout is required, as the canvas element is not yet available when the component mounts. For my purposes, the short 2.5s timer provides enough time for the event handler to latch onto the canvas.
I'm building a small iOS App in Swift and I use the WKWebView. What I am trying to achieve is to be notified when a web page has rendered completely. It is a known issue with WKWebView that none of its loading notifications work.
However, this approach seems to work and the idea is to hook into the window.onload function in Javascript and notify Swift from there. However, here too I found an issue. If I use JQuery, in some pages the callback doesn't happen apparently because the web pages already define window.onload. If I use the pure Javascript way, some web pages break i.e. they don't load at all apparently because I'm overriding the window.onload.
// using JQuery, this function never get called for web pages that do window.onload =
$(window).load(function() {
window.webkit.messageHandlers.callbackHandler.postMessage(
JSON.stringify({body: "window finished loading"}));
});
// works always but breaks web pages that use window.onload
window.onload = function() {
window.webkit.messageHandlers.callbackHandler.postMessage(
JSON.stringify({body: "window finished loading"}));
};
The question is how can I append this line notification to an existing window.onload and define one window.onload if it doesn't exist? other ideas also welcome e.g. queuing window.load implementations?
For completeness I have included below the two known ways to get such notifications using WKWebView natively.
(1) getting WKNavigationDelegate life-cycle notifications but the callback notification triggers too early when the web page has not yet rendered completely.
class ViewController: UIViewController {
// ...
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
NSLog("didFinishNavigation callback received ... too early!")
}
}
(2) Using the Key-Value Observer method (KVO) but here again callback notification triggers too early too:
webView.addObserver(viewController, forKeyPath: "estimatedProgress", options: .New, context: nil)
//
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<()>) {
guard let webView = object as? WKWebView else {return}
guard let change = change else {return}
guard let keyPath = keyPath else {return}
switch keyPath {
case "estimatedProgress":
if ((1.0 - webView.estimatedProgress) < 1e-10) {
NSLog("'estimatedProgress' callback received ... too early!")
}
break
default: break
}
}
it is the same issue for the "loading" KVO.
UPDATE: in addition to the accepted answer, the top ranked answer to this other question running-jquery-after-all-other-js-has-executed solves this OP too by polling the DOM for a specific change e.g. when all the Javascript has completed in addition to the loading of the page.
Instead of the load event you can listen to DOMContentLoaded.
Also you can perform the callback at the end of the execution stack by doing the following:
window.setTimeout(callback, 0);
Additionally you can try calling removeEventListener in your callback.
For Example:
if (window.addEventListener) {
var documentIsReady = function() {
window.removeEventListener("load", documentIsReady);
if (typeof window.isMyiOSAppAlreadyNotified === 'undefined') {
window.webkit.messageHandlers.callbackHandler.postMessage(JSON.stringify({body: "window onload"}));
}
window.isMyiOSAppAlreadyNotified = true;
};
window.addEventListener("load", function() { window.setTimeout(documentIsReady, 0); });
}
To avoid polluting the global(window) scope with variables like isMyiOSAppAlreadyNotified you can apply the module pattern.
I found a way that works. However despite my futile attempt to avoid duplicate notifications I still get two notifications for some web pages and haven't found a way to fix it yet ...
if (window.addEventListener) {
window.addEventListener("load",
function() {
// try to make sure it is called only once ...
if (typeof window.isMyiOSAppAlreadyNotified === 'undefined') {
// notify my iOS App that the page has finished loading
window.webkit.messageHandlers.callbackHandler.postMessage(
JSON.stringify({body: "window onload"}));
}
window.isMyiOSAppAlreadyNotified = true;
}
);
}
I'm currently developing a web page with a scrolling parallax effect (Stellar.js) on the header and three other sections of the site: however, scrolling them causes lag, especially at the top of the page.
I've already tried to reduce the background images' size by using compression, but it hasn't made too much difference; removing the blur effect didn't solve the problem, either (it did reduce the lag, but it still wasn't smooth enough).
The website runs pretty well on Firefox (W10), with almost no frame drops, but there's quite some lag on Chrome (both Windows and OS X) and Safari.
There's a few JS scroll-triggered scripts running, but I don't know if those may be the cause. Any suggestions?
What you're going to want to do is throttle scroll events. Debouncing events means an event can't fire again until after a certain amount of time. Throttling events means that the event can only fire so much per period of time.
Here's function to throttle events (credit: http://sampsonblog.com/749/simple-throttle-function)
// Create the listener function
function throttle (callback, limit) {
var wait = false; // Initially, we're not waiting
return function () { // We return a throttled function
if (!wait) { // If we're not waiting
callback.call(); // Execute users function
wait = true; // Prevent future invocations
setTimeout(function () { // After a period of time
wait = false; // And allow future invocations
}, limit);
}
}
}
To use it just do something like this:
function callback () {
console.count("Throttled");
}
window.addEventListener("scroll", throttle( callback, 200 ));
Working on a site use the flexslider plugin.
I want to detect a browser resize, and then re initiate the slider so it can adjust size / other args.
To do this, I found:
var slider = $('.flexslider').data('flexslider');
slider.resize();
Which works, but I have now created a function to call this and used it with a bind on the window resize:
$(window).bind('resize', function() {
console.log('window resized');
resizeSlider();
});
// resize the slider
function resizeSlider()
{
var slider = $('.flexslider').data('flexslider');
slider.resize();
console.log('slider-resized');
}
The problem is, when I resize the browser I get a constant stream of 'browser resized' logs and then a stack of 600 before:
Uncaught RangeError: Maximum call stack size exceeded
How can I fix this?
Sounds like there is an underlying problem we know nothing about. Perhaps the window resize event is triggered somewhere else?
Here's a small function I use throughout a lot of projects, helps me everytime.
/**
* #description delay events with the same id, good for window resize events, keystroke, etc ...
* #param {Function} func : callback function to be run when done
* #param {Integer} wait : integer in milliseconds
* #param {String} id : unique event id
*/
var delayedEvent = (function () {
var timers = {};
return function (func, wait, id) {
wait = wait || 200;
id = id || 'anonymous';
if (timers[id]) {
clearTimeout(timers[id]);
}
timers[id] = setTimeout(func, wait);
};
})();
To apply to your resize function, simply wrap it around:
function resizeSlider() {
delayedEvent(function() {
var slider = $('.flexslider').data('flexslider');
slider.resize();
console.log('slider-resized');
}, 200, 'flexslider-resize');
}
Now whenever the resizeSlider() is called, you'll execute the function every 200ms. This should prevent the range error.
To check for underlying problems, use it in the $(window).bind('resize').
Now I notice, have you tried $(window).on('resize') or what's the reason for bind here? You use an older jQuery version?
Also note that the $('.flexslider').data('flexslider') is called everytime. This variable should be outside the function scope, as a parameter perhaps, if it doesn't change. => "delegation" or "lambda" comes to mind.
Can you please try doing this, your code has very poor performance, as it is called permanently even if you move your window just a little bit. This might cause your error.
Add a delay in the resize method so your client can handle all the resizes.
I am not sure if this will fix your problem right away, but is very advisable to use it anyways, even if it won't fix your problem!
I'm using dragswipe, a plugin from here. It works perfectly fine. However, I have a requirement to do dynamic carousels. So, every time a user changes something on a page, the carousels get updated dynamically. I thought I could just recall the plugin to update the element, but somehow when I dragged the carousel the functional gets called multiple times.
For example, I do this to initialise the plugin.
function init() {
$('#carousel').dragswipe({
width: 320,
current_page_element: '#current_page',
total_pages_element:'#total_pages'
});
}
So after the page is updated via ajax I have a callback to call this method again like this
function callback() {
init();
}
Everything should is updated perfectly fine, but when I started dragging. The carousel skips some pages. I thought I had to unbind all the events so I tried this. $('#carousel').unbind() but the problem still persists.
Here's the source code when the plugin is initialised.
$.fn.dragswipe = function(options) {
options = $.extend({
offset: 0,
turn_threshold: 0.1,
current_page: 0
},options)
this.each(function() {
option = $(this).hammer({
drag_vertical: false,
swipe_time: 20
});
// start a drag.
// if we're moving left or right, set our initial distance to 0.
$(this).bind('dragstart',function(ev) {
console.log('dragstart');
if (ev.direction=='left' || ev.direction=='right') {
drag_distance = 0;
}
});
// while dragging, change the dragging distance based on how far a user has dragged.
$(this).bind('drag',function(ev) {
console.log('drag');
if (ev.direction=='left' || ev.direction=='right') {
drag_distance = ev.distanceX;
var options = CONFIGS[$(this).data('dragswipe_id')];
$(this).updateOffset(options.offset+drag_distance);
}
});
$(this).bind('dragend',function(ev) {
console.log('dragend');
if (ev.direction=='left' || ev.direction=='right') {
var options = CONFIGS[$(this).data('dragswipe_id')];
if (Math.abs(drag_distance / options.width) > options.turn_threshold) {
if (ev.direction=='left') {
options.current_page++;
}
if (ev.direction=='right') {
options.current_page--;
}
}
// store modified options
CONFIGS[$(this).data('dragswipe_id')] = options;
console.log(options.current_page);
$(this).gotoPage(options.current_page,true);
}
});
// set the dragswipe ID used to look up config options later.
$(this).data('dragswipe_id',CONFIGS.length);
// store config options.
CONFIGS[$(this).data('dragswipe_id')] = options;
});
}
Which I see nothing wrong with the plugin, but maybe I'm missing something obvious.
UPDATED
I have created the example in jsfiddle, but it's not working just in case anyone can fix the problem. Also, on the plugin site itself. The problem can be reproduce by running this code to initialise the plugin multiple times. After running the code twice, when you drag the page it goes to the last page instead of the second page.
$('#carousel').dragswipe({
width: 320,
current_page_element: '#current_page',
total_pages_element:'#total_pages'
});
Have you tried turning it off and on again?
Dragswipe has a removeDragswipe() method that does a little bit more than just unbinding, and it also explicitly only unbinds events that are related to dragswipe itself so it won't mess up any other events that may have been bound.
So instead of unbind(), do this:
function callback() {
$('#carousel').removeDragswipe();
init();
}
As discussed in the comments, this doesn't work because it seems that even after unbinding all the events, after re-binding them by activating the plugin again the events seem to fire an extra time.
What does seem to be a workaround is to actually rebuild the entire element so that no events can linger:
function callback() {
$('#carousel').removeDragswipe();
rebuildCarousel();
init();
}
function rebuildCarousel() {
var wrapper = $('#carousel').parent;
var html = parent.html();
$('#carousel').remove();
wrapper.html(html);
}