Skip to content

Commit 10d961d

Browse files
authored
Fix (Carousel): Improve frontend performance (#3561)
* improve carousel frontend performance removed swapping of slides * clone necessary slides only * remove unnecessary fx * use transform: TranslateX() * remove computedstyle * add zIndex
1 parent f4d4494 commit 10d961d

File tree

1 file changed

+173
-100
lines changed

1 file changed

+173
-100
lines changed

src/block/carousel/frontend-carousel.js

Lines changed: 173 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,6 @@ class _StackableCarousel {
4646

4747
this.fixChildrenAccessibility() // This needs to be first before infinte scrolling clones slides.
4848
this.initProperties()
49-
this.addEventListeners()
50-
this.fixAccessibility( this.currentSlide )
51-
this.setDotActive( this.currentSlide )
52-
this.fixInlineScrollNavigation()
53-
54-
this.slideEls[ this.currentSlide - 1 ].classList.add( 'stk-block-carousel__slide--active' )
55-
56-
this.unpauseAutoplay()
5749
}
5850

5951
initProperties = () => {
@@ -73,29 +65,97 @@ class _StackableCarousel {
7365
}
7466
}
7567

68+
// If we have infiniteScroll, call this after cloning the slides
69+
const otherInitCalls = () => {
70+
this.addEventListeners()
71+
this.fixAccessibility( this.currentSlide )
72+
this.setDotActive( this.currentSlide )
73+
this.fixInlineScrollNavigation()
74+
75+
this.slideEls[ this.currentSlide - 1 ].classList.add( 'stk-block-carousel__slide--active' )
76+
77+
this.unpauseAutoplay()
78+
}
79+
7680
if ( this.infiniteScroll && ! this.el._StackableHasInitCarousel ) {
7781
// clone slides
78-
this.clones = this.slideEls.map( node => node.cloneNode( true ) )
82+
this.clones = []
83+
const clonesToAdd = []
84+
let lastClone = null
85+
let index = 0
86+
let step = 0
87+
88+
const runInitSteps = () => {
89+
if ( step === 0 ) {
90+
// Clone only the last slide and the first N slides (where N equals to slidesToShow)
91+
const slideIndex = index === this.slidesToShow ? this.slideEls.length - 1 : index
92+
const original = this.slideEls[ slideIndex ]
93+
const clone = original.cloneNode( true )
94+
clone.classList.add( `stk-slide-clone-${ slideIndex + 1 }` )
95+
clone.style.zIndex = -1
96+
original.style.willChange = 'transform'
97+
original.style.transform = 'TranslateX( 0 )'
98+
99+
// prevents flickering when changing the value of TranslateX
100+
original.style.transition = 'transform 0s'
101+
102+
this.clones.push( clone )
103+
104+
if ( index === this.slidesToShow ) {
105+
lastClone = clone
106+
step++
107+
} else {
108+
clonesToAdd.push( clone )
109+
}
110+
111+
index++
112+
} else if ( step === 1 ) {
113+
// Append clones at the end except for the last slide clone which will be placed at the front
114+
this.sliderEl.append( ...clonesToAdd )
115+
if ( lastClone ) {
116+
this.sliderEl.insertBefore( lastClone, this.slideEls[ 0 ] )
117+
}
118+
step++
119+
} else if ( step === 2 ) {
120+
const numSlides = this.slideEls.length
121+
const slideClientRect = this.slideEls[ 0 ].getBoundingClientRect()
122+
const slideWidth = slideClientRect.width
123+
124+
this.slideTranslateX = `calc((${ slideWidth }px * ${ numSlides }) + (var(--gap) * ${ numSlides }))`
125+
126+
step++
127+
} else if ( step === 3 ) {
128+
// IMPORTANT: Do style reads before applying style change to improve performance
129+
// https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing
130+
const targetOffsetLeft = this.slideEls[ 0 ].offsetLeft
131+
132+
// Scroll without animation to the first slide
133+
this.sliderEl.style.scrollBehavior = 'unset'
134+
this.sliderEl.scrollLeft = targetOffsetLeft
135+
step++
136+
} else if ( step === 4 ) {
137+
this.sliderEl.style.scrollBehavior = ''
79138

80-
this.clones.map( ( node, i ) => {
81-
node.classList.add( `stk-slide-clone-${ i + 1 }` )
82-
if ( i === this.clones.length - 1 ) {
83-
return this.sliderEl.insertBefore( node, this.slideEls[ 0 ] )
139+
this.currentSlide = 1
140+
this.swappedSlides = 0
141+
this.updateDots()
142+
step++
143+
} else if ( step === 5 ) {
144+
otherInitCalls()
145+
step++
84146
}
85147

86-
return this.sliderEl.appendChild( node )
87-
} )
88-
89-
// Scroll without animation to the first slide
90-
this.sliderEl.style.scrollBehavior = 'unset'
91-
this.sliderEl.scrollLeft = this.slideEls[ 0 ].offsetLeft
92-
this.sliderEl.style.scrollBehavior = ''
148+
if ( step <= 5 ) {
149+
requestAnimationFrame( runInitSteps )
150+
}
151+
}
93152

94-
this.currentSlide = 1
95-
this.swappedSlides = 0
153+
requestAnimationFrame( runInitSteps )
154+
return
96155
}
97156

98157
this.updateDots()
158+
otherInitCalls()
99159
}
100160

101161
updateDots = () => {
@@ -198,7 +258,7 @@ class _StackableCarousel {
198258
nextSlide = () => {
199259
let newSlide = this.currentSlide + 1
200260

201-
if ( this.infiniteScroll && newSlide > this.maxSlides() ) {
261+
if ( this.type === 'slide' && this.infiniteScroll && newSlide > this.maxSlides() ) {
202262
this.swapSlides( newSlide, 'N' )
203263
return
204264
}
@@ -212,7 +272,7 @@ class _StackableCarousel {
212272
prevSlide = () => {
213273
let newSlide = this.currentSlide - 1
214274

215-
if ( this.infiniteScroll &&
275+
if ( this.type === 'slide' && this.infiniteScroll &&
216276
( newSlide < this.slideOffset || this.needToSwapCount( newSlide ) >= 0 ) ) {
217277
this.swapSlides( newSlide, 'P' )
218278
return
@@ -225,95 +285,100 @@ class _StackableCarousel {
225285
}
226286

227287
swapSlides = ( slide, dir ) => {
228-
let setScrollToClone = false
229-
if ( this.slidesToShow === this.slideEls.length ) {
230-
setScrollToClone = true
231-
}
232-
288+
let scrollToSlide = false
233289
if ( dir === 'N' && slide > this.slideEls.length ) {
234290
slide = this.slideOffset
235-
setScrollToClone = true
291+
scrollToSlide = true
236292
} else if ( dir === 'P' && slide < this.slideOffset ) {
237293
slide = this.slideEls.length
238-
setScrollToClone = true
294+
scrollToSlide = true
239295
}
240296

241297
const needToSwap = this.needToSwapCount( slide )
298+
let startIndex = 0
299+
let endIndex = 0
300+
let slideTranslateXValue = 0
242301
if ( needToSwap > 0 && this.swappedSlides < needToSwap ) {
243-
// swap original and clone slides
244-
const original = [ ...this.slideEls.slice( this.swappedSlides, needToSwap ) ]
245-
const clones = [ ...this.clones.slice( this.swappedSlides, needToSwap ) ]
246-
247-
original.map( node => this.sliderEl.insertBefore( node, this.clones[ needToSwap ] ) )
248-
clones.map( node => this.sliderEl.insertBefore( node, this.slideEls[ needToSwap ] ) )
249-
250-
// This ensures that the cloned slides are in the right position when slides to show === number of slides
251-
if ( this.slidesToShow === this.slideEls.length && dir === 'N' ) {
252-
const children = this.sliderEl.children
253-
this.sliderEl.append( children[ 0 ] )
254-
} else if ( this.slidesToShow === this.slideEls.length && dir === 'P' ) {
255-
const children = [ ...Array.from( this.sliderEl.children ).slice( -2 ) ].reverse()
256-
children.map( node => this.sliderEl.insertBefore( node, this.sliderEl.children[ 0 ] ) )
257-
}
302+
startIndex = this.swappedSlides
303+
endIndex = needToSwap
304+
305+
slideTranslateXValue = this.slideTranslateX
258306

259-
this.swappedSlides = needToSwap
307+
this.swappedSlides = endIndex
260308
} else if ( this.swappedSlides > needToSwap ) {
261-
// unswap original and clone slides that are not needed
262-
const _needToSwap = needToSwap > 0 ? needToSwap : 0
263-
const original = [ ...this.slideEls.slice( _needToSwap, this.swappedSlides ) ]
264-
const clones = [ ...this.clones.slice( _needToSwap, this.swappedSlides ) ]
265-
original.map( node => this.sliderEl.insertBefore( node, this.slideEls[ this.swappedSlides ] ) )
266-
clones.map( node => this.sliderEl.insertBefore( node, this.clones[ this.swappedSlides ] ) )
267-
this.swappedSlides = _needToSwap
268-
269-
// This ensures that the cloned slides are in the right position when slides to show === number of slides
270-
if ( this.slidesToShow === this.slideEls.length ) {
271-
const children = this.sliderEl.children
272-
this.sliderEl.insertBefore( children[ children.length - 1 ], children[ 0 ] )
273-
}
309+
startIndex = needToSwap > 0 ? needToSwap : 0
310+
endIndex = this.swappedSlides
311+
312+
this.swappedSlides = startIndex
274313
}
275314

276-
if ( setScrollToClone ) {
277-
// Move from the last slide to the first slide (N - next) or
278-
// Move from the first slide to the last slide (P - prev)
279-
this.sliderEl.style.scrollBehavior = 'unset'
280-
this.sliderEl.scrollLeft = dir === 'N'
281-
? this.clones[ this.currentSlide - 1 ].offsetLeft // Go to the last clone slide
282-
: ( this.slidesToShow === 1
283-
? this.clones[ this.currentSlide - 1 ].offsetLeft // If slidesToShow is 1, go to the first clone slide
284-
: this.slideEls[ this.currentSlide - 1 ].offsetLeft // Go to the original first slide which is swapped with the clone
285-
)
286-
this.sliderEl.style.scrollBehavior = ''
315+
let step = 0
316+
317+
const runSteps = () => {
318+
if ( step === 0 ) {
319+
this.slideEls.slice( startIndex, endIndex ).forEach( slide => {
320+
slide.style.transform = `TranslateX(${ slideTranslateXValue })`
321+
} )
322+
step++
323+
requestAnimationFrame( runSteps )
324+
} else if ( step === 1 ) {
325+
this.slideEls.slice( startIndex, endIndex ).forEach( slide => slide.offsetLeft )
326+
327+
if ( scrollToSlide ) {
328+
const lastCloneSlide = this.clones[ this.clones.length - 1 ].offsetLeft
329+
const firstCloneSide = this.clones[ 0 ].offsetLeft
330+
331+
let initSlide = null
332+
333+
if ( dir === 'N' ) {
334+
initSlide = lastCloneSlide
335+
} else if ( this.slidesToShow === 1 ) {
336+
initSlide = lastCloneSlide
337+
} else {
338+
initSlide = firstCloneSide
339+
}
340+
341+
// Move from the last slide to the first slide (N - next) or
342+
// Move from the first slide to the last slide (P - prev)
343+
this.sliderEl.style.scrollBehavior = 'unset'
344+
this.sliderEl.scrollLeft = initSlide
345+
this.sliderEl.style.scrollBehavior = ''
346+
}
347+
348+
step++
349+
requestAnimationFrame( runSteps )
350+
} else {
351+
requestAnimationFrame( () => this.goToSlide( slide ) )
352+
}
287353
}
288354

289-
setTimeout( () => {
290-
this.goToSlide( slide )
291-
}, 1 )
355+
requestAnimationFrame( runSteps )
292356
}
293357

294358
goToSlide = ( slide, force = false ) => {
295359
if ( slide === this.currentSlide && ! force ) {
296360
return
297361
}
362+
const currentSlideEl = this.slideEls[ this.currentSlide - 1 ]
363+
const newSlideEl = this.slideEls[ slide - 1 ]
364+
const offsetLeft = newSlideEl.offsetLeft
298365

299-
this.slideEls[ this.currentSlide - 1 ].classList.remove( 'stk-block-carousel__slide--active' )
300-
this.slideEls[ slide - 1 ].classList.add( 'stk-block-carousel__slide--active' )
366+
currentSlideEl.classList.remove( 'stk-block-carousel__slide--active' )
367+
newSlideEl.classList.add( 'stk-block-carousel__slide--active' )
301368

302369
if ( this.type === 'slide' ) {
303-
this.sliderEl.scrollLeft = this.slideEls[ slide - 1 ].offsetLeft
370+
this.sliderEl.scrollLeft = offsetLeft
304371
} else if ( this.type === 'fade' ) { // fade
305-
const slidePrevEl = this.slideEls[ this.currentSlide - 1 ]
306-
slidePrevEl.style.opacity = 0
307-
308-
const slideEl = this.slideEls[ slide - 1 ]
309-
slideEl.style.zIndex = ++this.currentZIndex
310-
slideEl.style.transition = 'none'
311-
slideEl.style.opacity = 0
312-
slideEl.style.visibility = 'visible'
313-
slideEl.style.left = `${ this.isRTL ? '' : '-' }${ 100 * ( slide - 1 ) }%`
372+
currentSlideEl.style.opacity = 0
373+
374+
newSlideEl.style.zIndex = ++this.currentZIndex
375+
newSlideEl.style.transition = 'none'
376+
newSlideEl.style.opacity = 0
377+
newSlideEl.style.visibility = 'visible'
378+
newSlideEl.style.left = `${ this.isRTL ? '' : '-' }${ 100 * ( slide - 1 ) }%`
314379
setTimeout( () => {
315-
slideEl.style.transition = ''
316-
slideEl.style.opacity = 1
380+
newSlideEl.style.transition = ''
381+
newSlideEl.style.opacity = 1
317382
}, 1 )
318383
}
319384
this.fixAccessibility( slide )
@@ -414,6 +479,11 @@ class _StackableCarousel {
414479
}
415480

416481
onWheel = e => {
482+
const sliderElScrollLeft = this.sliderEl.scrollLeft
483+
const lastSlideOffset = this.slideEls[ this.slideEls.length - 1 ].offsetLeft
484+
const firstCloneOffset = this.clones[ 0 ].offsetLeft
485+
const slidesOffset = this.slideEls.map( slide => slide.offsetLeft )
486+
const clonesOffset = this.clones.map( slide => slide.offsetLeft )
417487
if ( this.type === 'fade' ) {
418488
if ( this.wheelTimeout ) {
419489
return
@@ -430,19 +500,19 @@ class _StackableCarousel {
430500
}, 500 )
431501
}
432502
// For infinite scrolling, set the scroll position to the actual slide ( not to the clone of the slide )
433-
} else if ( this.infiniteScroll && e.deltaX <= -1 && this.sliderEl.scrollLeft === 0 ) {
503+
} else if ( this.infiniteScroll && e.deltaX <= -1 && sliderElScrollLeft === 0 ) {
434504
this.sliderEl.style.scrollBehavior = 'unset'
435-
this.sliderEl.scrollLeft = this.slideEls[ this.slideEls.length - 1 ].offsetLeft
505+
this.sliderEl.scrollLeft = lastSlideOffset
436506
this.sliderEl.style.scrollBehavior = ''
437-
} else if ( this.infiniteScroll && e.deltaX >= 1 && this.sliderEl.scrollLeft >= this.clones[ 0 ].offsetLeft ) {
438-
this.clones.every( ( clone, i ) => {
439-
if ( this.sliderEl.scrollLeft === clone.offsetLeft ) {
507+
} else if ( this.infiniteScroll && e.deltaX >= 1 && sliderElScrollLeft >= firstCloneOffset ) {
508+
clonesOffset.some( ( offset, i ) => {
509+
if ( sliderElScrollLeft === offset ) {
440510
this.sliderEl.style.scrollBehavior = 'unset'
441-
this.sliderEl.scrollLeft = this.slideEls[ i ].offsetLeft
511+
this.sliderEl.scrollLeft = slidesOffset[ i ]
442512
this.sliderEl.style.scrollBehavior = ''
443-
return false
513+
return true
444514
}
445-
return true
515+
return false
446516
} )
447517
}
448518
}
@@ -467,14 +537,17 @@ class _StackableCarousel {
467537
dragMouseMove = e => {
468538
// How far the mouse has been moved
469539
let dx = e.clientX - this.initialClientX
540+
const sliderElScrollLeft = this.sliderEl.scrollLeft
541+
const lastSlideOffsetLeft = this.slideEls[ this.slideEls.length - 1 ].offsetLeft
542+
const firstCloneOffsetLeft = this.clones[ 0 ].offsetLeft
470543

471544
if ( this.type === 'slide' ) {
472-
if ( this.infiniteScroll && this.sliderEl.scrollLeft === 0 && dx > 0 ) {
473-
this.initialScrollLeft = this.slideEls[ this.slideEls.length - 1 ].offsetLeft
545+
if ( this.infiniteScroll && sliderElScrollLeft === 0 && dx > 0 ) {
546+
this.initialScrollLeft = lastSlideOffsetLeft
474547
this.initialClientX = e.clientX
475548
dx = 0
476-
} else if ( this.infiniteScroll && this.sliderEl.scrollLeft >= this.clones[ 0 ].offsetLeft && dx < 0 ) {
477-
this.initialScrollLeft = this.slideEls[ 0 ].offsetLeft
549+
} else if ( this.infiniteScroll && sliderElScrollLeft >= firstCloneOffsetLeft && dx < 0 ) {
550+
this.initialScrollLeft = firstCloneOffsetLeft
478551
this.initialClientX = e.clientX
479552
dx = 0
480553
}

0 commit comments

Comments
 (0)