I am trying to implement a sticky header row for table, but need to do it without position: sticky property.
The approach I am going for is translating Y coordinate of header row along with document vertical scroll by same amount of pixels.
As the document is scrolled down, we translate the header row downwards by same number of pixels giving the effect that header row is stuck at the top of table.
I have a working implementation at https://codepen.io/shubham_687/pen/porPwbP
PFA the gist of implementation below
canvas.onscroll = function (e) {
if (canvas.scrollTop >= rect.top) {
const numOfPixels = canvas.scrollTop - rect.top;
element.setAttribute(
"style",
"transition:0s;transform: translate3d(0px," + numOfPixels + "px, 0px);"
);
} else {
element.setAttribute(
"style",
"transition:0s;transform: translate3d(0px, 0px, 0px);"
);
}
};
The issue I am facing is that the implementation is jerky/sluggish/jumpy in some browsers and mobile devices while working perfectly fine in some (Google chrome).
As per my understanding, this behavior might be because of the following reason(s)
Large number of scroll events per second causing perf impact, might be possible to solve using debouncing, any suggestions?
Updating css dynamically leads to update layer tree which takes some time as well and the number of times this gets called is directly proportional to the times scroll event is registered.
Please share possible suggestions for perf improvement.
Related
first of all I want to say that it's not about content jumping!
I have a navbar and a sidebar which both have absolute position. after user scrolls 100 pixels I change both of them to fixed. but an odd action happens (not always!). wrappers of navbar and sidebar flush for a second. I tested it with different browsers and it does not depend on browser. I tried to reproduce the situation in this fiddle:
https://jsfiddle.net/addxmkgj/
(resize the screen as large as possible it happens in large screens)
-- Edit --
https://codepen.io/anon/pen/dJKBPe
codepen link added too.
Causes
Scrolling can generate scroll events quickly and handlers may need to either throttle scroll events to some extent (e.g. perform code action after scrolling has stopped) or be fairly lightweight functions that can execute quickly.
In addition scroll event handling is not synchronized with page update: if the mouse wheel initiates downward scrolling, scrolling can continue after the wheel is released (and similarly with touch event scrolling). The browser can scroll below a top position of 100px before scroll event handling has had a chance to catch up and change the positioning.
The result is the header jumps down from being partially off-screen to occupy a fixed position at top of screen. The faster the scroll action (or the busier the browser is) the more likely it is that jumping will be noticeable.
A secondary effect in desktop browsing is that when the side bar panel scrolls upwards past top of screen and moves down again, a visible patch of white screen "flashes" momentarily below the side bar before fixed positioning takes effect.
Experimental Remedies
Flashing of the side bar can be reduced but not necessarily fully eliminated, by increasing the height of the container. Changing the height to 150% with visible overflow met with some success:
.side-bar {
position: absolute;
height: 150%;
... /* more declarations */
This may or may not conflict with application requirements.
Some mitigation of navbar jumping can be achieved by using requestAnimationFrame call backs to monitor scrollTop values and change positioning as necessary. This does not use scroll event handling as such:
$(document).ready(function() {
$(window).resize(function() {
if( $(window).width() > 850) {
$('.navbar').css('display', 'block');
} else {
$('.navbar').css('display', 'none');
}
});
scrollTo(0, 0);
var num = 100;
var bAbsolute = true;
function checkScroll() {
var newTop = $(window).scrollTop();
if( bAbsolute && newTop >= num) {
$('.navbar').css('position', 'fixed');
$('.navbar').css('top', '0');
$('.side-bar').css('position', 'fixed');
$('.side-bar').css('top', '0');
bAbsolute = false;
}
if( !bAbsolute && newTop < num) {
$('.navbar').css('position', 'absolute');
$('.side-bar').css('position', 'absolute');
$('.navbar').css('top', '100px');
$('.side-bar').css('top', '100px');
bAbsolute = true;
}
requestAnimationFrame( checkScroll);
}
requestAnimationFrame( checkScroll)
});
This code showed an improvement in jump reduction but was not perfect. It is not particularly a JQuery solution and calls requestAnimationFrame directly.
One option, of course, is to do nothing given browser timing constraints.
Update
This MDN guide for Scroll linked effects explains the root cause problem better than I was able to:
most browsers now support some sort of asynchronous scrolling .... the visual scroll position is updated in the compositor thread and is visible to the user before the scroll event is updated in the DOM and fired on the main thread ... This can cause the effect to be laggy, janky, or jittery — in short, something we want to avoid.
So the absolutely positioned elements can scroll off screen (to some extent) before scroll handlers are notified of a new scroll position.
The solution going forward is to use sticky positioning (see the scroll effects guide above or the CSS position guide. However position:sticky swaps between relative and fixed position so the HTML would need redesigning to accommodate this.
Sticky positioning is also leading edge technology at January 2018, and not yet recommended for production use on MDN. A web search for "JQuery support sticky position" revealed a choice of JQuery plugin support.
Recommendation
Potentially the best-case compromise may be to redesign the HTML to use sticky positioning and include a JQuery plugin that uses native support when available or a polyfill when not - site visitors with supporting browsers will get the best experience, those with older browsers will get functional support.
For a project of mine (see BigPictu.re or bigpicture.js GitHub project), I have to deal with potentially a very, very, very big <div> container.
I knew there was a risk of poor performance with the simple approach I use, but I did not expect it to be mostly present with ... Chrome only!
If you test this small page (see code below), panning (click + drag) will be:
Normal / smooth on Firefox
Normal / smooth even on Internet Explorer
Very slow (nearly crashing) on Chrome!
Of course, I could add some code (in my project) to do that when you're zoomed in a lot, text with potentially very very big font-size would be hidden. But still, why does Firefox and Internet Explorer handle it correctly and not Chrome?
Is there a way in JavaScript, HTML, or CSS to tell the browser not to try to render the whole page (which is 10000 pixels wide here) for every action? (only render the current viewport!)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
html, body {
overflow: hidden;
min-height: 100%; }
#container {
position: absolute;
min-height: 100%;
min-width: 100%; }
.text {
font-family: "Arial";
position: absolute;
}
</style>
</head>
<body>
<div id="container">
<div class="text" style="font-size: 600px; left:100px; top:100px">Small text</div>
<div class="text" style="font-size: 600000px; left:10000px; top:10000px">Very big text</div>
</div>
<script>
var container = document.getElementById('container'), dragging = false, previousmouse;
container.x = 0; container.y = 0;
window.onmousedown = function(e) { dragging = true; previousmouse = {x: e.pageX, y: e.pageY}; }
window.onmouseup = function() { dragging = false; }
window.ondragstart = function(e) { e.preventDefault(); }
window.onmousemove = function(e) {
if (dragging) {
container.x += e.pageX - previousmouse.x; container.y += e.pageY - previousmouse.y;
container.style.left = container.x + 'px'; container.style.top = container.y + 'px';
previousmouse = {x: e.pageX, y: e.pageY};
}
}
</script>
</body>
</html>
Changing to position: fixed seems to speed things up.
Use transform instead of top/left:
container.style.transform = 'translate(' + container.x + 'px, ' + container.y + 'px)';
A live demo at jsFiddle.
Answer to first quest "why". One of problems are font size. you have font size 600000px, most browser will see it as too high and render smaller, while chrome tries to render original size. Looks like chrome can not repaint such big letters with your requested styles very fast.
But combining Teemu and geert3 answers - using transform and position:fixed, makes chrome works much more faster even with big fonts.
Answer to 2nd question: "Is there a way ... not to try to render the whole page" - you can try to apply mouse action for elements in container, not for whole container.
Maximum font sizes: http://jsfiddle.net/74w7yL0a/
firefox 34 - 2 000 px
chrome 39 - 1 000 000 px
safari 8 - 1 000 000 px
ie 8-11 - 1 431 700 px
In addition to Teemu's answer of using translate:
container.style.transform = 'translate(' + container.x + 'px, ' + container.y + 'px)';
Which you should also use other vendor prefixes, You can simply fix this by using this on the body:
height: 100%;
width: 100%;
position: relative;
overflow: hidden;
and this on html:
height: 100%;
this will, however, disable scrolling. So what I'd do is, add a mousedown event to the body and apply those styles using a css class whenever mousedown is triggered, and removing that class on mouseup.
#Teemus' answer almost does it all.
Use transform with translate3d instead of top/left.
translate3d enables hardware acceleration.
container.style.transform = 'translate3d(' + container.x + 'px, ' + container.y + 'px, 0)';
A live demo at jsFiddle.
I analyzed this and I found that the original problem related to the Chrome display architecture, and its use of background threads to render the page.
If you want to have fast rendering, go into chrome:flags, scroll to the setting Impl-side painting, and set "Disabled", then restart the browser - the mousemove will be smooth.
What I found is that if you enable the FPS counter, the reported FPS in this scenario is still very high, even though the actual onscreen performance is very low. My tentative explanation (not being a Chrome display architecture expert) is that if the UI thread and display are on separate threads, then there can be contention in the rendering of the div - in the case where the UI thread and rendering thread is on the same thread, the UI thread cannot send messages faster than the UI thread can render.
I would suggest that this should be filed as a Chrome bug.
Use display: table and table-layout:fixed on the div, or a table wrapping the div. In HTML:
The HTML table model has been designed so that, with author assistance, user agents may render tables incrementally (i.e., as table rows arrive) rather than having to wait for all the data before beginning to render.
In order for a user agent to format a table in one pass, authors must tell the user agent:
The number of columns in the table. Please consult the section on calculating the number of columns in a table for details on how to supply this information.
The widths of these columns. Please consult the section on calculating the width of columns for details on how to supply this information.
More precisely, a user agent may render a table in a single pass when the column widths are specified using a combination of COLGROUP and COL elements. If any of the columns are specified in relative or percentage terms (see the section on calculating the width of columns), authors must also specify the width of the table itself.
For incremental display, the browser needs the number of columns and their widths. The default width of the table is the current window size (width="100%"). This can be altered by setting the width attribute of the TABLE element. By default, all columns have the same width, but you can specify column widths with one or more COL elements before the table data starts.
The remaining issue is the number of columns. Some people have suggested waiting until the first row of the table has been received, but this could take a long time if the cells have a lot of content. On the whole it makes more sense, when incremental display is desired, to get authors to explicitly specify the number of columns in the TABLE element.
Authors still need a way of telling user agents whether to use incremental display or to size the table automatically to fit the cell contents. In the two pass auto-sizing mode, the number of columns is determined by the first pass. In the incremental mode, the number of columns must be stated up front (with COL or COLGROUP elements).
and CSS:
17.5.2.1 Fixed table layout
With this (fast) algorithm, the horizontal layout of the table does not depend on the contents of the cells; it only depends on the table's width, the width of the columns, and borders or cell spacing.
The table's width may be specified explicitly with the 'width' property. A value of 'auto' (for both 'display: table' and 'display: inline-table') means use the automatic table layout algorithm. However, if the table is a block-level table ('display: table') in normal flow, a UA may (but does not have to) use the algorithm of 10.3.3 to compute a width and apply fixed table layout even if the specified width is 'auto'.
References
HTML 4: 11.2, Elements for constructing tables
HTML 4: Appendix B, Section 5.1.2: Incremental Display
HTML 5: Algorithm for Processing Rows
CSS2: 17.5.2 Table width algorithms, the 'table-layout' property
Draft U.S. Web Design Standards Documentation | Tables
I'm currently working on a new site build, currently located at http://weve.wpengine.com/. As part of the site design, I've implemented a simple backbone.js view that scrolls the background at a different speed to the rest of the content to create a perspective illusion.
weve.ScrollTransitionView = Backbone.View.extend({
templateId: 'template-page-background',
className: 'page-background',
initialize: function () {
var scope = this;
var factor = 1.3;
jQuery(window).scroll(function (e) {
var scrollTop = jQuery(this).scrollTop();
var position = 'top ' + (scrollTop / factor) + 'px center';
scope.$el.css({ 'background-position': position });
});
}
});
This works fine in Firefox. However, in Chrome, there is a ripple effect upon scrolling - the background seems to only partially render in places and bits of the image get rendered out of place - and in I.E. the effect is jittery.
Initially I thought this may be an issue with the skewed elements on the page but after creating a mockup using partially transparent images rather than CSS skewed DOM elements I experienced the same problems.
Also, the mockup appeared to be fine when there were only a few elements on the page but when I translated this to the site where there were more text/images/elements on the page the rendering issue persisted.
I've found hints here and there in this and other forums for related rendering issues but nothing which seems to match the problem I'm experiencing.
Can anybody help?
I eventually found a solution to this. The problem (for me) was related to the way that the Chrome rendering engine works (See: http://www.html5rocks.com/en/tutorials/speed/layers/).
Essentially, the foreground of my page was divided into a series of sections that contained transformed (skewed) elements. Even though the sections themselves appeared as diagonals, the rendering engine treated them as squares with triangular transparent regions on opposing corners. When the background moved, these sections weren't being changed - which meant the rendering engine, in an effort to be faster and more efficient, wasn't re-rendering the transparent sections properly with the new background position, creating the jitter effect.
The solution was to add a line to my parallax javascript to subtly modify the css of each of the transformed sections each time the background changed. I set the script to adjust the background of the div element on each loop of the parallax code, alternating between two colours - rgba(255, 0, 255, 0) and rgba(255, 255, 0, 0) - which were different in theory but in practice 100% transparent. This forced the browser to re-render properly and solved the jitter problem.
I am using DataTables for a large quantity of data gathered from various ASP databases.
In addition, I am using the following DataTables plug-ins:
FixedHeaders, FixedColumns, ColReorder.
Now, with the ColReorder function, when a header (a column actually) is dragged to reorder it and is overflowing to the sides (horizontal scrolling), I would like the table to scroll to the side with it, so that the column can be dragged not only to the viewed area of the table but to the entire table.
I have tried nomerous ways and walkarounds such as trying to make it scroll when the curser is close to the boundaries (like here in "Drag Scrolls" http://javascriptmvc.com/docs.html#!jQuery.Drag), but I did not succeed, as well as this method - http://mootools.net/docs/more/Interface/Scroller.
I would highly appreciate your help.
Thank you all in advance.
Here is an approach that may get you closer to solving it. Based on my experiments, you can copy and modify dataTables.colReorder.js in function _fnMouseMove just before this.dom.drag.css(...) code.
var scrollLeftVal = $(this.s.dt.nTableWrapper).find('.dataTables_scrollBody').scrollLeft();
if((e.pageX - this.s.mouse.offsetX) > (this.s.dt.nTableWrapper.clientWidth - 50)) {
scrollLeftVal += 10;
$(this.s.dt.nTableWrapper).find('.dataTables_scrollBody').scrollLeft(scrollLeftVal);
}
This will scroll the window to the right when dragging a column header. A reverse method would be needed as well, as well as modifying the positions of the pointer and drag elements, such as below:
this.dom.pointer.css( 'left', this.s.aoTargets[i-1].x - scrollLeftVal );
If you decided on this approach, you would be better off long term to build an extension/plugin rather than modifying core code. This is just for experimental purposes.
I started down this path, but believe I'll do my own reordering interface since this approach, even if working and scrolling properly, is not as ideal for the project I'm working on.
Hope this helps someone get closer to a solution!
Ryan
Tested with bootstrap https://jsfiddle.net/bababalcksheep/gsf2r1v4/17/
I added this code by modifying dataTables.colReorder.js in function _fnMouseMove just before this.dom.drag.css(...)
Seems to work Fine however with only one problem.
Lengthy columns have hard time snapping to next short width column.
For Example: in https://jsfiddle.net/bababalcksheep/gsf2r1v4/17/ , try dragging project column to next EXT column. I suspect this is because project column has larger width.
Any further fix to this problem will be much appreciated
var scrollHead = $(this.s.dt.nTableWrapper).find('.dataTables_scrollHead');
var scrollBody = $(this.s.dt.nTableWrapper).find('.dataTables_scrollBody');
var scrollLeftVal = e.pageX - scrollHead.parent().offset().left - this.s.mouse.offsetX;
//
var difference = scrollHead[0].clientWidth - scrollLeftVal ;
//is near Right edge, scroll to far right
if (difference < 150){
scrollLeftVal = scrollHead[0].scrollWidth - scrollHead[0].clientWidth;// max scrollleft Value
}
//is near Left edge, scroll to far Left
if(scrollLeftVal < 100){
scrollLeftVal= 0;
}
//
scrollBody.scrollLeft(scrollLeftVal);
I am trying to re-create website with parallax effect using JavaScript. That means that I have two or more layers, that are moving different speeds while scrolling.
In my case I'm moving only one layer, the other one remains static:
layer 1 = website text;
layer 2 = element background;
for this I'm using simple source code (with jQuery 1.6.4):
var docwindow = $(window);
function newpos(pos, adjust, ratio){
return ((pos - adjust) * ratio) + "px";
}
function move(){
var pos = docwindow.scrollTop();
element.css({'top' : newpos(pos, 0, 0.5)});
}
$(window).scroll(function(){
move();
});
The Problem:
- All calculations are done right and the effect "works" as expected. But there is some performance glitch under some browsers (Chrome MAC/Windows, Opera MAC, IE, paradoxically not Safari).
What do I see during scrolling?
- While scrolling the background moves in one direction together with scroll, but it seems to occasionally jump few pixels back and then forth, which creates very disturbing effect (not fluid).
Solutions that I tried:
- adding a timer to limit scroll events
- using .animate() method with short duration instead of .css() method.
I've also observed, that the animation is smooth when using .scrollTo method (scrollTo jQuery plugin). So I suspect that there is something wrong with firing scroll events (too fast).
Have you observed the same behavior?
Do you know, how to fix it?
Can you post a better solution?
Thanks for all responses
EDIT #1:
Here you can find jsfiddle demonstration (with timer): http://jsfiddle.net/4h9Ye/1/
I think you should be using scrollTop() instead and change the background position to fixed. The problem is that setting it to absolute will make it move by default when you scroll up or down.
The flicker occurs because the position is updated as part of the default browser scroll and updated again as part of your script. This will render both positions instead of just the one you want. With fixed, the background will never move unless you tell it so.
I've created a demo for you at http://jsfiddle.net/4h9Ye/2/ .