Khi xây dựng dashboard trong Oracle APEX, chúng ta thường dùng Tabs hoặc Region Display Selector
để chia một page lớn thành nhiều nhóm nội dung.
Trên giao diện, người dùng chỉ nhìn thấy một tab tại một thời điểm.
Vì vậy có một giả định rất dễ mắc phải:
Tab đang bị ẩn thì query hoặc PL/SQL của tab đó chắc chưa chạy.
Tuy nhiên, trong một bài kiểm tra thực tế, tôi phát hiện Oracle APEX vẫn có thể render nội dung
của tất cả các tab ngay trong initial page request.
Tab không active chỉ bị ẩn ở browser.
Điều này trở thành vấn đề performance nếu các tab chứa query lớn, REST API,
PL/SQL Dynamic Content hoặc logic xử lý mất nhiều thời gian.
Bài viết này mô phỏng lại quá trình kiểm tra, cách chứng minh bằng APEX Debug,
và cách triển khai lazy load để chỉ query dữ liệu khi người dùng thực sự mở tab.
1. Bài toán
Tạo một page test trong Oracle APEX gồm ba tab:
- COURSES
- TEACHER COURSES
- STUDENT COURSES
Mỗi tab sử dụng một bảng có lượng dữ liệu tương đối lớn.
| Tab |
Table |
Số record trong môi trường test |
| COURSES |
COURSES |
194,617 |
| TEACHER COURSES |
TEACHER_COURSES |
4,786,882 |
| STUDENT COURSES |
STUDENT_COURSES |
1,873,300 |
Tab mặc định khi page load là COURSES.
Câu hỏi cần xác định:
Khi page vừa mở, APEX chỉ chạy code của COURSES
hay code của TEACHER COURSES và STUDENT COURSES cũng chạy?
2. Điều người dùng thấy và điều server thực sự làm
Browser
COURSES
ACTIVE
TEACHER COURSES
HIDDEN
STUDENT COURSES
HIDDEN
↓
Initial Page Render Request
✓
Query / Render COURSES
✓
Query / Render TEACHER_COURSES
✓
Query / Render STUDENT_COURSES
Điểm quan trọng:
Hidden trên browser không đồng nghĩa với Not Rendered trên server.
3. Kiểm tra bằng DOM
Khi inspect HTML của APEX Tabs Region, tab active có trạng thái tương tự:
<div
id="SR_tab_courses"
role="tabpanel"
aria-hidden="false">
...
</div>
Trong khi tab chưa active vẫn tồn tại trong DOM:
<div
id="SR_tab_teacher_courses"
role="tabpanel"
aria-hidden="true"
style="display: none;">
...
</div>
Nếu bên trong panel đã có HTML của report hoặc Dynamic Content,
đó là dấu hiệu region đã được server render trước đó.
display:none chỉ làm browser không hiển thị nội dung.
Nó không làm SQL hoặc PL/SQL phía server biến mất.
4. Kiểm tra bằng Network
Một phép thử đơn giản khác:
- Load page ở tab COURSES.
- Mở Chrome DevTools → Network.
- Chọn Fetch/XHR.
- Clear Network.
- Click TEACHER COURSES.
Nếu tab hiển thị đầy đủ dữ liệu ngay nhưng không xuất hiện request lớn dùng để lấy HTML hoặc dữ liệu,
khả năng cao nội dung đã được tải sẵn từ initial page render.
Một vài request wwv_flow.ajax nhỏ vẫn có thể xuất hiện do APEX framework,
Developer Toolbar hoặc plugin.
Không nên chỉ nhìn tên request; cần xem payload, response, size và thời gian xử lý.
5. Tạo bài test có thể đo được bằng APEX Debug
Để chứng minh chính xác region nào chạy, mỗi tab được đổi sang PL/SQL Dynamic Content
kiểu PL/SQL Function Body returning a CLOB.
COURSES
DECLARE
l_count NUMBER;
l_start NUMBER := DBMS_UTILITY.GET_TIME;
l_clob CLOB;
BEGIN
apex_debug.message(
'TEST_TAB_START: COURSES'
);
SELECT COUNT(*)
INTO l_count
FROM courses;
l_clob :=
'<h3>COURSES</h3>'
|| '<p>Records: '
|| l_count
|| '</p>';
apex_debug.message(
'TEST_TAB_END: COURSES - records=%s - elapsed=%s sec',
l_count,
ROUND(
(DBMS_UTILITY.GET_TIME - l_start) / 100,
2
)
);
RETURN l_clob;
END;
TEACHER COURSES
DECLARE
l_count NUMBER;
l_start NUMBER := DBMS_UTILITY.GET_TIME;
l_clob CLOB;
BEGIN
apex_debug.message(
'TEST_TAB_START: TEACHER_COURSES'
);
SELECT COUNT(*)
INTO l_count
FROM teacher_courses;
l_clob :=
'<h3>TEACHER COURSES</h3>'
|| '<p>Records: '
|| l_count
|| '</p>';
apex_debug.message(
'TEST_TAB_END: TEACHER_COURSES - records=%s - elapsed=%s sec',
l_count,
ROUND(
(DBMS_UTILITY.GET_TIME - l_start) / 100,
2
)
);
RETURN l_clob;
END;
STUDENT COURSES
DECLARE
l_count NUMBER;
l_start NUMBER := DBMS_UTILITY.GET_TIME;
l_clob CLOB;
BEGIN
apex_debug.message(
'TEST_TAB_START: STUDENT_COURSES'
);
SELECT COUNT(*)
INTO l_count
FROM student_courses;
l_clob :=
'<h3>STUDENT COURSES</h3>'
|| '<p>Records: '
|| l_count
|| '</p>';
apex_debug.message(
'TEST_TAB_END: STUDENT_COURSES - records=%s - elapsed=%s sec',
l_count,
ROUND(
(DBMS_UTILITY.GET_TIME - l_start) / 100,
2
)
);
RETURN l_clob;
END;
6. Bật App Trace và kiểm tra initial request
Bật:
Developer Toolbar
→ Debug
→ App Trace
Sau đó refresh page và chưa click TEACHER COURSES hoặc STUDENT COURSES.
Trong View Debug, mở request có:
Search:
Kết quả cho thấy cả ba region đều execute trong cùng initial request.
COURSES
Records: 194,617
EXECUTED ✓
TEACHER_COURSES
Records: 4,786,882
EXECUTED ✓
STUDENT_COURSES
Records: 1,873,300
EXECUTED ✓
Kết luận:
dù browser chỉ đang hiển thị COURSES, PL/SQL của cả ba tab đã chạy.
7. COUNT(*) vẫn quá nhanh để thấy rõ ảnh hưởng performance
Trong môi trường test, các câu COUNT(*) vẫn chạy khá nhanh.
Để tạo một controlled test dễ nhìn, thêm:
vào mỗi tab ngay sau message START.
Ví dụ:
apex_debug.message(
'TEST_TAB_START: COURSES'
);
DBMS_SESSION.SLEEP(3);
SELECT COUNT(*)
INTO l_count
FROM courses;
Lưu ý:
DBMS_SESSION.SLEEP chỉ được dùng trong bài test DEV/UAT để mô phỏng workload.
Không để delay giả này trong production.
8. Kết quả trước khi Lazy Load
Sau khi mỗi tab delay khoảng 3 giây:
Initial Page Load - Before Lazy Load
0s
3s
6s
9s
User chỉ đang xem COURSES nhưng phải chờ:
≈ 9.7 seconds
APEX Debug ghi nhận request page có elapsed time khoảng:
Trong khi Maximum Execution Time của một individual operation chỉ khoảng 3 giây.
Điều này cho thấy ba region đang được xử lý nối tiếp trong initial request.
9. Mục tiêu của Lazy Load
Thay vì:
PAGE LOAD
↓
COURSES ~3s
↓
TEACHER COURSES ~3s
↓
STUDENT COURSES ~3s
↓
PAGE READY ~9.7s
Ta muốn:
PAGE LOAD
↓
COURSES ~3s
↓
PAGE READY
Click Teacher
↓
Load Teacher ~3s
Click Student
↓
Load Student ~3s
Chi phí xử lý không biến mất.
Nó được dời đến thời điểm người dùng thực sự cần dữ liệu.
10. Tạo flag điều khiển từng tab
Tạo hai page item:
P1024_LOAD_TEACHER
P1024_LOAD_STUDENT
Giá trị mặc định:
Khi người dùng mở Teacher lần đầu:
Khi mở Student:
11. Sửa PL/SQL của Teacher Courses
Region Teacher ban đầu chỉ return placeholder.
DECLARE
l_count NUMBER;
l_start NUMBER := DBMS_UTILITY.GET_TIME;
l_clob CLOB;
BEGIN
IF NVL(:P1024_LOAD_TEACHER, 'N') <> 'Y'
THEN
RETURN
'<div class="lazy-placeholder">'
|| 'Click tab to load Teacher Courses...'
|| '</div>';
END IF;
apex_debug.message(
'TEST_TAB_START: TEACHER_COURSES'
);
/* Chỉ dùng để test performance */
DBMS_SESSION.SLEEP(3);
SELECT COUNT(*)
INTO l_count
FROM teacher_courses;
l_clob :=
'<h3>TEACHER COURSES</h3>'
|| '<p>Records: '
|| l_count
|| '</p>';
apex_debug.message(
'TEST_TAB_END: TEACHER_COURSES - records=%s - elapsed=%s sec',
l_count,
ROUND(
(DBMS_UTILITY.GET_TIME - l_start) / 100,
2
)
);
RETURN l_clob;
END;
Trong property Page Items to Submit của region Teacher:
Static ID của region:
12. Sửa PL/SQL của Student Courses
DECLARE
l_count NUMBER;
l_start NUMBER := DBMS_UTILITY.GET_TIME;
l_clob CLOB;
BEGIN
IF NVL(:P1024_LOAD_STUDENT, 'N') <> 'Y'
THEN
RETURN
'<div class="lazy-placeholder">'
|| 'Click tab to load Student Courses...'
|| '</div>';
END IF;
apex_debug.message(
'TEST_TAB_START: STUDENT_COURSES'
);
/* Chỉ dùng để test performance */
DBMS_SESSION.SLEEP(3);
SELECT COUNT(*)
INTO l_count
FROM student_courses;
l_clob :=
'<h3>STUDENT COURSES</h3>'
|| '<p>Records: '
|| l_count
|| '</p>';
apex_debug.message(
'TEST_TAB_END: STUDENT_COURSES - records=%s - elapsed=%s sec',
l_count,
ROUND(
(DBMS_UTILITY.GET_TIME - l_start) / 100,
2
)
);
RETURN l_clob;
END;
Page Items to Submit:
Static ID:
13. Tại sao không dùng Server-side Condition để loại bỏ region?
Một ý tưởng tự nhiên là đặt condition để Teacher và Student hoàn toàn không render lúc page load.
Nhưng nếu region không tồn tại trong DOM,
sau đó JavaScript sẽ không có region instance để gọi:
apex.region(
"tab_teacher_courses"
).refresh();
Vì vậy cách được dùng trong bài test là:
- Region vẫn tồn tại.
- Initial render chỉ return một placeholder rất nhẹ.
- Khi user mở tab, page item đổi thành Y.
- Ajax refresh region.
- Lúc đó mới chạy query thật.
14. Bắt sự kiện chuyển tab
Trong quá trình test, việc bind trực tiếp click vào tab header không hoạt động ổn định
với cấu trúc Tabs Region hiện tại.
Ví dụ selector vẫn tìm được element:
$("#SR_tab_teacher_courses_tab").length
// 1
Nhưng event click trực tiếp không phải tín hiệu đáng tin cậy trong case này.
Quan sát DOM cho thấy APEX luôn thay đổi:
aria-hidden="true"
↓
aria-hidden="false"
khi một panel trở thành tab active.
Vì vậy có thể quan sát thuộc tính này bằng MutationObserver.
15. Đặt Static ID cho Tabs Region cha
Region cha:
Các ID dùng trong solution:
| Component |
ID |
| Tabs container |
test_tabs |
| Teacher panel do APEX tạo |
SR_tab_teacher_courses |
| Student panel do APEX tạo |
SR_tab_student_courses |
| Teacher region Static ID |
tab_teacher_courses |
| Student region Static ID |
tab_student_courses |
16. JavaScript Lazy Load hoàn chỉnh
Đặt đoạn code sau tại:
Page
→ JavaScript
→ Execute when Page Loads
(function () {
const tabsContainer =
document.getElementById(
"test_tabs"
);
if (!tabsContainer) {
return;
}
function lazyLoadTab(
panelId,
itemName,
regionId
) {
const panel =
document.getElementById(
panelId
);
if (!panel) {
return;
}
const isActive =
panel.getAttribute(
"aria-hidden"
) === "false";
if (
isActive
&&
$v(itemName) !== "Y"
) {
$s(
itemName,
"Y"
);
apex.region(
regionId
).refresh();
}
}
function checkTabs() {
lazyLoadTab(
"SR_tab_teacher_courses",
"P1024_LOAD_TEACHER",
"tab_teacher_courses"
);
lazyLoadTab(
"SR_tab_student_courses",
"P1024_LOAD_STUDENT",
"tab_student_courses"
);
}
const observer =
new MutationObserver(
checkTabs
);
observer.observe(
tabsContainer,
{
subtree: true,
attributes: true,
attributeFilter: [
"aria-hidden"
]
}
);
/*
* Trường hợp page load mà một
* lazy tab đã active sẵn.
*/
checkTabs();
})();
17. Flow sau khi áp dụng Lazy Load
PAGE LOAD
Chỉ COURSES chạy query thật
↓
COURSES
Query + Render ≈ 3s
↓
PAGE READY ≈ 3.7s
TEACHER COURSES
User mở tab lần đầu
↓
aria-hidden = false
↓
MutationObserver
↓
P1024_LOAD_TEACHER = Y
↓
apex.region(...).refresh()
↓
Query TEACHER_COURSES
STUDENT COURSES
User mở tab lần đầu
↓
aria-hidden = false
↓
MutationObserver
↓
P1024_LOAD_STUDENT = Y
↓
apex.region(...).refresh()
↓
Query STUDENT_COURSES
18. Tại sao tab chỉ load một lần?
Khi Teacher được load thành công:
Lần sau người dùng chuyển về Teacher, điều kiện:
$v("P1024_LOAD_TEACHER") !== "Y"
không còn đúng.
Do đó apex.region(...).refresh() không chạy lại.
Student sử dụng cơ chế tương tự.
Tab nặng chỉ query một lần khi được mở lần đầu,
sau đó việc chuyển qua lại chỉ là thao tác UI.
19. Kết quả Before / After
BEFORE
COURSES
3.01s
TEACHER
3.15s
STUDENT
3.03s
≈ 9.7s
→
AFTER
COURSES
~3s
TEACHER
Lazy
STUDENT
Lazy
≈ 3.7s
Initial page load trong controlled test giảm khoảng
6 seconds
từ khoảng 9.7s xuống khoảng 3.7s.
Đây không phải benchmark chung cho Oracle APEX.
Con số trên đến từ controlled test có DBMS_SESSION.SLEEP(3)
để làm rõ sự khác biệt giữa eager rendering và lazy loading.
Mức cải thiện thực tế phụ thuộc query, API, số region và workload của ứng dụng.
20. Khi nào Lazy Load đặc biệt hữu ích?
Kỹ thuật này có giá trị lớn khi tab chứa:
- SQL query lớn.
- Interactive Report hoặc Classic Report phức tạp.
- PL/SQL Dynamic Content.
- REST API hoặc Web Service.
- Procedure gọi hệ thống bên ngoài.
- Nhiều vòng lặp để tạo HTML.
- Chart hoặc report mà user không phải lúc nào cũng xem.
- Nhiều tab độc lập trên cùng dashboard.
Nếu một tab chỉ mất vài millisecond,
lazy load có thể không tạo khác biệt đáng kể.
Nhưng nếu một dashboard có 5–10 tab và mỗi tab tốn vài trăm millisecond
hoặc vài giây, initial page load có thể tăng nhanh.
21. Production checklist
Không để DBMS_SESSION.SLEEP trong production.
Đặt Static ID rõ ràng cho region cha và region cần refresh.
Khai báo Page Items to Submit cho các flag lazy load.
Kiểm tra initial page bằng APEX Debug / App Trace.
Kiểm tra Network để chắc chắn tab đang load bằng Ajax khi cần.
Không refresh lại region nếu dữ liệu đã được load và không cần cập nhật.
Nếu dữ liệu phải luôn mới, có thể thay flag Y/N bằng chiến lược refresh phù hợp hơn.
Đo performance trước và sau thay vì chỉ dựa vào cảm giác trên UI.
22. Một số lưu ý khi áp dụng vào page thật
Bài test sử dụng page item:
P1024_LOAD_TEACHER
P1024_LOAD_STUDENT
Khi áp dụng vào page production, cần thay bằng item tương ứng của page thật.
Tương tự:
test_tabs
tab_teacher_courses
tab_student_courses
SR_tab_teacher_courses
SR_tab_student_courses
phải được map lại với Static ID và DOM ID thật của ứng dụng.
Không nên copy nguyên ID của page test sang production mà không kiểm tra DOM.
23. Vì sao cần đo bằng APEX Debug thay vì chỉ nhìn browser?
Browser giúp trả lời:
- HTML đã tồn tại chưa?
- Tab có phát sinh Ajax request mới không?
- Region nào đang hidden?
Nhưng APEX Debug mới giúp trả lời:
- PL/SQL region có thực sự execute không?
- Query hoặc process nào đang chiếm thời gian?
- Elapsed time của toàn request là bao nhiêu?
- Một operation cụ thể mất bao lâu?
Trong case này, chính App Trace đã chứng minh:
TEST_TAB_START: COURSES
TEST_TAB_END: COURSES
TEST_TAB_START: TEACHER_COURSES
TEST_TAB_END: TEACHER_COURSES
TEST_TAB_START: STUDENT_COURSES
TEST_TAB_END: STUDENT_COURSES
đều nằm trong cùng initial page request.
24. Bài học quan trọng
Đừng giả định một component không nhìn thấy trên UI
thì server chưa xử lý component đó.
Khi debug performance Oracle APEX, nên kiểm tra theo nhiều lớp:
- DOM.
- Network.
- APEX Debug / App Trace.
- SQL / PL/SQL execution time.
- Controlled test để cô lập nguyên nhân.
- Before / After measurement.
Cách tiếp cận này giúp tránh tối ưu theo cảm giác và tập trung vào phần thực sự tạo latency.
25. Kết luận
Tabs trong Oracle APEX giúp tổ chức UI rất tốt,
nhưng tab đang hidden không mặc nhiên có nghĩa region đó được lazy load.
Trong bài test này:
Before
COURSES ~3s
TEACHER ~3s
STUDENT ~3s
---------------------
Initial Load ~9.7s
Sau khi chuyển hai tab không mặc định sang cơ chế placeholder + Ajax refresh:
After
Initial Page
→ COURSES only
→ ~3.7s
Open Teacher first time
→ Ajax load Teacher
Open Student first time
→ Ajax load Student
Open loaded tab again
→ No additional query
Đây là một kỹ thuật hữu ích cho những Oracle APEX dashboard có nhiều tab,
nhiều report hoặc nhiều source dữ liệu nặng.
Quan trọng nhất không phải là MutationObserver hay một đoạn JavaScript cụ thể,
mà là tư duy:
Chỉ thực hiện công việc nặng khi người dùng thực sự cần nó,
và luôn đo lại performance sau khi thay đổi.